はじめに
C++のバージョンが上がるにつれて、constexpr
の制限はどんどん緩和されていきました。その結果、std::vector
やstd::unique_ptr
はconstexpr
対応されました。
std::shared_ptr
は未だconstexpr
対応されていませんが、C++26に向けて、P3037R3 constexpr std::shared_ptr が提案されています。
この記事では、std::shared_ptr
をconstexpr
対応するにあたって必要なコア言語機能と、実装上の注意点を見ていきます。
constexpr
new
/delete
C++20から定数式でnew
/delete
式が呼び出せるようになりました。P0784R7
他にもstd::allocator
/std::allocator_traits
のconstexpr
対応などが含まれており、定数式での動的なコンテナへの道が開かれました。
まずはこれがなければ始まりませんね。
constexpr
virtual
function
C++20から定数式で仮想関数を呼び出せるようになりました。P1064R0
std::shared_ptr
はものすごく簡略化すると、
class ref_count_base
{
virtual void destroy() = 0;
};
template <typename T>
class ref_count : ref_count_base
{
T* m_ptr;
void destroy() override
{
delete m_ptr;
}
};
template <typename T>
class shared_ptr
{
ref_count_base* m_ref_count;
public:
template <typename U>
shared_ptr(U* p)
{
m_ref_count = new ref_count<U>(p);
}
};
のような構造になっており、仮想関数を通して、正しいデストラクタを呼び出せるようになっています。
そのため、constexpr virtual
は必須です。
アトミック操作
shared_ptr
は参照カウントを管理していますが、スレッドセーフにするため、参照カウントの増減をするときに排他処理をする必要があります。これにはアトミック操作を使用している実装が多いです。アトミック操作は通常、constexpr
でないので、この部分も対応する必要があります。
といっても、定数に評価されているときは排他処理をする必要がないので、通常のインクリメント/デクリメントを行えば良いだけです。
class ref_count_base
{
int m_use_count;
constexpr void increment_use_count()
{
if (std::is_constant_evaluated())
{
// 定数式の中では普通にインクリメントする
++m_use_count;
return;
}
AtomicIncrement(&m_use_count); // プラットフォームごとのアトミック操作
}
};
make_shared
/allocate_shared
make_shared
やその仲間達は、対象となるオブジェクトと参照カウントオブジェクトの両方が収まる大きさのメモリを一度に確保し、reinterpret_cast
でそれぞれのオブジェクトのポインタを得る、という関数ですが、reinterpret_cast
は定数式の中で使用することはできないので、そのままではconstexpr
にできません。
といっても、定数式の中であればメモリ確保の回数を減らす必要が無いため、通常のnew
を使った確保をすれば良いだけです。
template <typename T, typename... Args>
constexpr shared_ptr<T>
make_shared(Args&&... args)
{
if (std::is_constant_evaluated())
{
// 定数式の中では普通に初期化する
return shared_ptr<T>(new T(std::forward<Args>(args)...));
}
// メモリを一度に確保する実装
// ...
}
ここまで見てきたことからわかるように、コア言語機能的にはC++20の時点でshared_ptr
のかなりの部分をconstexpr
にすることができます。
ここからは、現時点ではconstexpr
にできない部分と、その他の注意点を見ていきます。
std::hash
/owner_hash
std::hash<std::shared_ptr<T>>
は、ポインタをreinterpret_cast
でsize_t
型に変換しているため、constexpr
にすることができません。
owner_hash
も同じ理由でconstexpr
にすることができません。
reinterpret_pointer_cast
reinterpret_pointer_cast
も、名前から明らかなようにreinterpret_cast
を使っているため、constexpr
にすることができません。
get_deleter
get_deleter
関数は次のような感じで実装されていますが、C++23までの時点ではconstexpr
にすることができません。
class ref_count_base
{
virtual void* get_deleter(const std::type_info ti) const = 0;
};
template <typename T, typename Deleter>
class ref_count : ref_count_base
{
Deleter m_deleter;
void* get_deleter(const std::type_info& ti) const override
{
if (ti == typeid(Deleter))
{
return &m_deleter;
}
return nullptr;
}
};
template <typename D, typename T>
D* get_deleter(shared_ptr<T> const& p)
{
void* p = p.m_ref_count->get_deleter(typeid(D));
return static_cast<D*>(p);
}
std::type_info::operator==
はC++23でconstexpr
になりましたので問題ありません。(P1328R1)
問題は最後のvoid*
からキャストしている箇所で、これは今のところconstexpr
では認められていません。これをできるように緩和しようと、C++26に向けて提案されています。
P2738R1 constexpr cast from void*: towards constexpr type-erasure
この提案が採用されれば、get_deleter
関数もconstexpr
にすることができます。
operator<=>
ポインタの大小比較でちゃんと定義された結果が返ってくるのは、同一配列内での比較などの限定された場合だけであり、それ以外では未規定の値となり実質的に定数式では使えません。
しかし、限られた場面であったとしても、有用なユースケースが有るとして、P3037R3 ではoperator<=>
をconstexpr
指定するように提案しています。ついでに(?)、unique_ptr
の大小比較についても、同様の理由でconstexpr
指定するように提案されています。
いずれにせよ、ライブラリ提供側としてはoperator<=>
をconstexpr
指定することに問題は無いと思います。
owner_before
owner_before
は通常、参照カウントオブジェクトへのポインタの大小比較として実装され、これはconstexpr
にはできないように思われます。
P3037R3 では特にこれについての言及がなくconstexpr
指定されているのですが、どうやったら実装できるのでしょうか…。ちょっとわかりません。
まとめ
- コア言語機能的にはC++20の時点で、
shared_ptr
のほとんどの部分はconstexpr
にできる - C++26では
get_deleter
もconstexpr
にできるかもしれない - 大小比較は条件付きで
constexpr
にできる
というわけで、拙作のライブラリでconstexpr
対応したshared_ptr
を実装しました。C++20以上であればconstexpr
で使えます。