C++26に向けて提案されているstd::inplace_vector
(P0843R14) を自作ライブラリに実装しました。
この記事では、実装する過程で気付いたことをシェアしようと思います。
inplace_vectorは全てのメンバ関数がconstexpr
指定されていますが、P0843R14には次のように記載されています。
For any N > 0, if
is_trivial_v<T>
is false, then noinplace_vector<T, N>
member functions are usable in constant expressions.
つまり型T
がトリビアル型でない場合、inplace_vector<T, N>
の全てのメンバ関数はconstexpr
にならないということです。これは一体どういうことでしょうか。
inplace_vectorを素直に実装すると次のような構造になります。
template <typename T, size_t N>
class inplace_vector
{
size_t m_size;
T m_value[N];
constexpr T* data() { return m_value; }
};
この実装はT
がint
やfloat
のようなスカラー型のときは問題ありません。
しかし、例えばT
がデフォルト構築可能でない場合、inplace_vector<T, N>
をインスタンス化しようとするとコンパイルエラーになります。T
の型によらず、inplace_vector
はデフォルト構築可能であってほしいので、これはまずいです。
また、デフォルトコンストラクタやデストラクタをユーザー定義している場合、inplace_vector
をデフォルト構築しただけでそれらの処理がN回呼び出されてしまいます。これはパフォーマンス上の問題となり得ます。
つまり、「T
がトリビアルにデフォルト構築可能かつトリビアルに破棄可能」でない場合、上記の実装では問題があります。
そこで、そういう場合はsizeof(T) * N
が収まるだけのバッファを確保し、そのバッファにT*
としてアクセスできるようにします。
template <typename T, size_t N>
class inplace_vector
{
size_t m_size;
alignas(T) char m_value[sizeof(T) * N];
T* data() { return reinterpret_cast<T*>(&m_value[0]); }
};
そして、is_trivial_v<T>
がtrue
かどうかで実装を分岐するようにします。
// T がトリビアル型のとき
template <typename T, size_t N, bool = is_trivial_v<T>>
class inplace_vector_base
{
size_t m_size;
T m_value[N];
constexpr T* data() { return m_value; }
};
// T がトリビアル型でないとき
template <typename T, size_t N>
class inplace_vector_base<T, N, false>
{
size_t m_size;
alignas(T) char m_value[sizeof(T) * N];
T* data() { return reinterpret_cast<T*>(&m_value[0]); }
};
template <typename T, size_t N>
class inplace_vector : inplace_vector_base<T, N>
{
// (省略)
};
これで、T
がトリビアル型でない場合でもインスタンス化できるようになります。その代償として、バッファの取得の部分でreinterpret_cast
を使っているため、constexpr
ではなくなります。これが冒頭に紹介した制限の正体です。
T
がトリビアル型でない場合でもconstexpr
にできるように、P3074R5が提案されています。
この中では未初期化の領域をconstexpr
で使えるようにするための方法が何種類か検討されていますが、いずれにせよ言語機能の変更が必要となっています。
P3074R5が採用されるまでは、条件付きのconstexpr
で我慢するしかなさそうです。