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で我慢するしかなさそうです。