4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

constexpr shared_ptr への道

Posted at

はじめに

C++のバージョンが上がるにつれて、constexprの制限はどんどん緩和されていきました。その結果、std::vectorstd::unique_ptrconstexpr対応されました。

std::shared_ptrは未だconstexpr対応されていませんが、C++26に向けて、P3037R3 constexpr std::shared_ptr が提案されています。

この記事では、std::shared_ptrconstexpr対応するにあたって必要なコア言語機能と、実装上の注意点を見ていきます。

constexpr new/delete

C++20から定数式でnew/delete式が呼び出せるようになりました。P0784R7

他にもstd::allocator/std::allocator_traitsconstexpr対応などが含まれており、定数式での動的なコンテナへの道が開かれました。

まずはこれがなければ始まりませんね。

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_castsize_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_deleterconstexprにできるかもしれない
  • 大小比較は条件付きでconstexprにできる

というわけで、拙作のライブラリでconstexpr対応したshared_ptrを実装しました。C++20以上であればconstexprで使えます。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?