constexpr shared_ptr への道

# はじめに

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

`std::shared_ptr`は未だ`constexpr`対応されていませんが、C++26に向けて、[P3037R3 constexpr std::shared_ptr](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3037r3.pdf) が提案されています。

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

## `constexpr` `new`/`delete`
C++20から定数式で`new`/`delete`式が呼び出せるようになりました。[P0784R7](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0784r7.html)

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

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

## `constexpr` `virtual` function
C++20から定数式で仮想関数を呼び出せるようになりました。[P1064R0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1064r0.html)

`std::shared_ptr`はものすごく簡略化すると、

```cpp
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`でないので、この部分も対応する必要があります。

といっても、定数に評価されているときは排他処理をする必要がないので、通常のインクリメント/デクリメントを行えば良いだけです。

```cpp
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`を使った確保をすれば良いだけです。

```cpp
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`にすることができません。

```cpp
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](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1328r1.html))

問題は最後の`void*`からキャストしている箇所で、これは今のところ`constexpr`では認められていません。これをできるように緩和しようと、C++26に向けて提案されています。
[P2738R1 constexpr cast from void*: towards constexpr type-erasure
](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2738r1.pdf)

この提案が採用されれば、`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`で使えます。

https://github.com/shibainuudon/HamonCore/blob/main/libs/memory/include/hamon/memory/shared_ptr.hpp