std::optional
やstd::variant
は保持する型がトリビアルな型であれば、そのトリビアル性を継承することが規格によって求められており、その実装には非常に難解なテクニックが使用されます。しかし、C++20以降、このテクニックは過去のものとなり忘れ去られていく事でしょう。この記事はそんなロストテクノロジーの記録です。
- メンバ型のトリビアル性を継承、とは?
- optional<T>簡易実装
- デストラクタ
- コピー/ムーブコンストラクタ
- 代入演算子
- デフォルトコンストラクタ
- C++20 Conditionally Trivial Special Member Functions
- なぜにトリビアル?
- 参考文献
メンバ型のトリビアル性を継承、とは?
テンプレートパラメータで指定された型の値をメンバとして保持するときに、そのテンプレートパラメータの型のトリビアル性を継承する事です。
template<typename T> struct wrap { T t; }; template<typename T> void f(wrap<T>) { // 要素型Tがトリビアルであれば static_assert(std::is_trivial_v<T>); // wrap<T>もトリビアルとなってほしい static_assert(std::is_trivial_v<wrap<T>>); }
トリビアルというのは、クラスの特殊メンバ関数がユーザーによって定義されていないことを言います(単純には)。これによって、trivially copyableならばmemcpy
できるようになるとか、trivially destructibleならばデストラクタ呼び出しを省略できる、などの保証が得られます。
上記のwrap<T>
型のように単純な型であれば単純にメンバとして保持しただけでも継承していますが、std::optional
のように複雑な型ではそうは行きません。しかしそれをなんとかする方法がちゃんと存在しています。
optional<T>
簡易実装
この記事ではoptional
の簡易実装によってメンバ型のトリビアル性継承がどのように行われるのかを見ていきますので、ここでベースとなる簡易実装rev1を書いておきます。
template<typename T> class my_optional { union { char dummy; T data; }; bool has_value = false; public: // デフォルトコンストラクタ constexpr my_optional() : dummy{} , has_value(false) {} // 値を受け取るコンストラクタ template<typename U=T> constexpr my_optional(U&& v) : data(std::forward<U>(v)) , has_value(true) {} // コピーコンストラクタ my_optional(const my_optional& that) : dummy{} , has_value(that.has_value) { if (that.has_value) { new (&this->data) T(that.data); } } // ムーブコンストラクタ my_optional(my_optional&& that) : dummy{} , has_value(that.has_value) { if (that.has_value) { new (&this->data) T(std::move(that.data)); } } // コピー代入演算子 my_optional& operator=(const my_optional& that) { auto copy = that; *this = std::move(copy); return *this; } // ムーブ代入演算子 my_optional& operator=(my_optional&& that) { if (this->has_value) { this->data.~T(); } this->has_value = that.has_value; if (that.has_value) { new (&this->data) T(std::move(that.data)); } return *this; } // デストラクタ ~my_optional() { if (has_value) { this->data.~T(); } } };
この実装はとりあえずoptional
っぽい働きはします。C++11で制限解除された共用体はそのメンバ型が非トリビアルな特殊メンバ関数を持つとき、対応する特殊メンバ関数がdelete
されます。そのため、それをラップする外側の型はそれを書いておく必要があります。optional
は遅延構築や任意タイミングでの無効値への切り替えが可能であり、それを実現するためには共用体を利用するのが最短でしょう。なお、状態を変化させるのは他のメンバ関数や代入演算子で行いますが、ここではそれは重要ではないので省略します。また、noexcept
については考えないことにします。
デストラクタ
簡易実装rev1はデストラクタがトリビアルではありません。T
がtrivially destructibleであるならばデストラクタ呼び出しは省略できるので、my_optional
のデストラクタもトリビアルに出来そうです。そしてそれは、C++17の世界でmy_optional
がconstexpr
となるための必要十分条件です。
デストラクタのトリビアル性継承は要するに、T
がトリビアルデストラクタを持つ場合にdefault
で、そうではない場合に独自定義、という風に分岐してやればいいのです。それはクラステンプレートの部分特殊化を用いて、次のように実装できます。
// デストラクタがトリビアルでない場合のストレージ template<typename T, bool = std::is_trivially_destructible_v<T>> struct optional_storage { union { char dummy; T data; }; bool has_value = false; // デストラクタは常に非トリビアルでdeleteされているので定義する ~optional_storage() { if (has_value) { this->data.~T(); } } }; // デストラクタがトリビアルである場合のストレージ template<typename T> struct optional_storage<T, true> { union { char dummy; T data; }; bool has_value = false; // デストラクタはトリビアルであり常にdeleteされないので、宣言すらいらない }; template<typename T> class my_optional : private optional_storage<T> { public: // 他略 // デストラクタ、この宣言も実はいらない ~my_optional() = default; };
optional_storage<T>
というクラスにデータを保持する部分を移管し、optional_storage<T>
はT
がtrivially destructibleである場合とない場合でテンプレートの部分特殊化によって実装を切り替えます。そしてその実装では、T
がtrivially destructibleである場合はデストラクタはトリビアルに定義され(ユーザー定義されず)、T
がtrivially destructibleでない場合に引き続きユーザー定義されます。これらの選択は与えられた型T
によって自動的に行われ、my_optional<T>
はT
のtrivially destructible性を継承します。
int main() { // パスする static_assert(std::is_trivially_destructible_v<my_optional<int>>); static_assert(std::is_trivially_destructible_v<my_optional<std::string>> == false); }
簡易実装rev2は次のようになりました。
// デストラクタがトリビアルでない場合のストレージ template<typename T, bool = std::is_trivially_destructible_v<T>> struct optional_storage { bool has_value = false; union { char dummy; T data; }; // デストラクタは常に非トリビアルでdeleteされているので定義する ~optional_storage() { if (has_value) { this->data.~T(); } } }; // デストラクタがトリビアルである場合のストレージ template<typename T> struct optional_storage<T, true> { bool has_value = false; union { char dummy; T data; }; // デストラクタはトリビアルであり常にdeleteされないので、宣言すらいらない }; template<typename T> class my_optional : private optional_storage<T> { public: // デフォルトコンストラクタ constexpr my_optional() : has_value(false) , dummy{} {} // 値を受け取るコンストラクタ template<typename U=T> constexpr my_optional(U&& v) : has_value(true) , data(std::forward<U>(v)) {} // コピーコンストラクタ my_optional(const my_optional& that) : has_value(that.has_value) , dummy{} { if (that.has_value) { new (&this->data) T(that.data); } } // ムーブコンストラクタ my_optional(my_optional&& that) : has_value(that.has_value) , dummy{} { if (that.has_value) { new (&this->data) T(std::move(that.data)); } } // コピー代入演算子 my_optional& operator=(const my_optional& that) { auto copy = that; *this = std::move(copy); return *this; } // ムーブ代入演算子 my_optional& operator=(my_optional&& that) { if (this->has_value) { this->data.~T(); } this->has_value = that.has_value; if (that.has_value) { new (&this->data) T(std::move(that.data)); } return *this; } };
コピー/ムーブコンストラクタ
コピー/ムーブコンストラクタをトリビアルに定義するとは、先程のデストラクタのようにT
でのそれがトリビアルならばmy_optional
でのそれもトリビアルとなるようにすればいいのです。が、冷静に考えてみると、すでにデストラクタのトリビアル性で分岐している所にコピーコンストラクタのそれでさらに分岐し、さらにムーブコンストラクタでも・・・となって組合せ爆発のようになることがわかるでしょう。じゃあいい方法が・・・ないので愚直に書きましょう。
ただ、そのような分岐を1つのクラスにまとめようとすると組合せ爆発で死ぬのは想像が付くので、特殊メンバ関数一つに対して1つのクラスが必要で、その1つのクラスにはdefault
によるトリビアルな定義をするものと自前定義するものの2つの特殊化が必要になりそうです。
もう少しよくよく考えてみると、T
のある特殊メンバ関数がトリビアルであるとき、基底となるoptional_storage
でもそれはトリビアルに定義できるはずなので、そこで定義されたそれを活用すればトリビアルケースの定義を省略出来る事に気づけます(私は気づきませんでしたが)。
コピーコンストラクタだけで見てみると、次のようになります。
// デストラクタがトリビアルでない場合のストレージ template<typename T, bool = std::is_trivially_destructible_v<T>> struct optional_storage { union { char dummy; T data; }; bool has_value = false; constexpr optional_storage() : dummy{} , has_value(false) {} template<typename... Args> constexpr optional_storage(Args&&... arg) : data(std::forward<Args>(arg)...) , has_value(true) {} // 定義できればトリビアル、そうでないなら暗黙delete optional_storage(const optional_storage&) = default; optional_storage(optional_storage&&) = default; optional_storage& operator=(const optional_storage&) = default; optional_storage& operator=(optional_storage&&) = default; ~optional_storage() { if (has_value) { this->data.~T(); } } template<typename... Args> void construct(Args&&... arg) { new (&this->data) T(std::forward<Args>(arg)...); has_value = true; } template<typename Self> void construct_from(Self&& that) { if (that.has_value) { // thatの値カテゴリを伝播する construct(std::forward<Self>(that).data); } } }; // デストラクタがトリビアルである場合のストレージ template<typename T> struct optional_storage<T, true> { union { char dummy; T data; }; bool has_value = false; constexpr optional_storage() : dummy{} , has_value(false) {} template<typename... Args> constexpr optional_storage(Args&&... arg) : data(std::forward<Args>(arg)...) , has_value(true) {} // 定義できればトリビアル、そうでないなら暗黙delete optional_storage(const optional_storage&) = default; optional_storage(optional_storage&&) = default; optional_storage& operator=(const optional_storage&) = default; optional_storage& operator=(optional_storage&&) = default; template<typename... Args> void construct(Args&&... arg) { new (&this->data) T(std::forward<Args>(arg)...); has_value = true; } template<typename Self> void construct_from(Self&& that) { if (that.has_value) { // thatの値カテゴリを伝播する construct(std::forward<Self>(that).data); } } }; template<typename T> struct enable_copy_ctor : optional_storage<T> { using base = optional_storage<T>; // ユーザー定義コピーコンストラクタ enable_copy_ctor(const enable_copy_ctor& that) : base() { this->construct_from(static_cast<const base&>(that)); } // 他のは全部基底のものか上で定義されるものに頼る! enable_copy_ctor() = default; enable_copy_ctor(enable_copy_ctor&&) = default; enable_copy_ctor& operator=(const enable_copy_ctor&) = default; enable_copy_ctor& operator=(enable_copy_ctor&&) = default; }; template<typename T> using check_copy_ctor = std::conditional_t< std::is_trivially_copy_constructible_v<T>, optional_storage<T>, enable_copy_ctor<T> >; template<typename T> class my_optional : private check_copy_ctor<T> { public: // 他略 // コピーコンストラクタ // copy_ctor_enabler<T>のコピーコンストラクタを利用する my_optional(const my_optional& that) = default; };
C++11以降の共用体は内包する型の特殊メンバ関数がトリビアルでないならば、対応する自身の特殊メンバ関数が暗黙delete
されます。従って、optional_storage
ではデストラクタ以外をとりあえず全部default
定義しておけば、トリビアルの時だけは定義されていることになります。
それを利用し、T
がtrivially copyableの時だけ、my_optional
からoptional_storage
に至るクラス階層にコピーコンストラクタをユーザー定義するクラスを追加し、そうでなければoptional_storage
を直接利用します。すると、最上位my_optional
クラスからはその基底クラスのコピーコンストラクタは常に何かしら定義されているように見えるため、my_optional
のコピーコンストラクタはdefault
で定義する事ができます。
派生クラスのコンストラクタ初期化子リストからは最基底のoptional_storage
のメンバは触れませんので、optional_storage
にはコンストラクタが必要です。また、フラグの管理とか構築周りのことを共通化するためにoptional_storage
にconstruct()/construct_from()
関数を追加しておきます。
同じようにムーブコンストラクタを定義しましょう。
template<typename T> struct enable_move_ctor : check_copy_ctor<T> { using base = check_copy_ctor<T> // ユーザー定義ムーブコンストラクタ enable_move_ctor(enable_move_ctor&& that) : base() { this->construct_from(static_cast<base&&>(that)); } // コピーコンストラクタはenable_copy_ctorで定義されるか // optional_storageでトリビアルに定義される enable_move_ctor(const enable_move_ctor&) = default; enable_move_ctor() = default; enable_move_ctor& operator=(const enable_move_ctor&) = default; enable_move_ctor& operator=(enable_move_ctor&&) = default; }; template<typename T> using check_move_ctor = std::conditional_t< std::is_trivially_move_constructible_v<T>, check_copy_ctor<T>, enable_move_ctor<T> >; template<typename T> class my_optional : private check_move_ctor<T> { public: // 他略 // ムーブコンストラクタ my_optional(my_optional&&) = default; };
my_optional
とcheck_copy_ctor
の間に、さっきと同じようなものを挿入してやるだけです、簡単ですね・・・
int main() { // パスする static_assert(std::is_trivially_destructible_v<my_optional<int>>); static_assert(std::is_trivially_copy_constructible_v<my_optional<int>>); static_assert(std::is_trivially_move_constructible_v<my_optional<int>>); static_assert(std::is_trivially_destructible_v<my_optional<std::string>> == false); static_assert(std::is_trivially_copy_constructible_v<my_optional<std::string>> == false); static_assert(std::is_trivially_move_constructible_v<my_optional<std::string>> == false); }
int
は当然トリビアルなクラスでありstd::string
は全ての特殊メンバ関数がそうではないので、このstatic_assert
群によってちゃんとトリビアル性が伝播されている事がわかります。
代入演算子
残ったのはコピー/ムーブ代入演算子です。これは特別な事をする必要はほぼなく、コンストラクタの時と同様のアプローチによって実装できます。
// デストラクタがトリビアルでない場合のストレージ template<typename T, bool = std::is_trivially_destructible_v<T>> struct optional_storage { // 中略 template<typename Self> void asign_from(Self&& that) { if (that.has_value) { if (this->has_value) { this->data = std::forward<Self>(that).data; } else { this->construct(std::forward<Self>(that).data); } } else { this->reset(); } } void reset() { if (this->has_value) { this->data.~T(); this->has_value = false; } } }; // デストラクタがトリビアルである場合のストレージ template<typename T> struct optional_storage<T, true> { // 中略 template<typename Self> void asign_from(Self&& that) { if (that.has_value) { if (this->has_value) { this->data = std::forward<Self>(that).data; } else { this->construct(std::forward<Self>(that).data); } } else { this->reset(); } } void reset() { this->has_value = false; } }; // 中略 template<typename T> struct enable_copy_asign : check_move_ctor<T> { using base = check_move_ctor<T>; // ユーザー定義コピー代入演算子 enable_copy_asign& operator=(const enable_copy_asign& that) { this->asign_from(static_cast<const base&>(that)); } enable_copy_asign() = default; enable_copy_asign(const enable_copy_asign&) = default; enable_copy_asign(enable_copy_asign&&) = default; enable_copy_asign& operator=(enable_copy_asign&&) = default; }; template<typename T> using check_copy_asign = std::conditional_t< std::is_trivially_copy_assignable_v<T>, check_move_ctor<T>, enable_copy_asign<T> >; template<typename T> class my_optional : private check_copy_asign<T> { public: // 他略 // コピー代入演算子 my_optional& operator=(const my_optional&) = default; };
代入演算子では自身の状態を一度無効化する必要がありますが、その処理はT
のデストラクタがトリビアルであるかによって変化しますので、代入に伴うあれこれと共にoptional_storage
に実装しておきます(asign_from()/reset()
)。
それを用いてenable_~_asign
クラスで代入演算子を実装します。まあ、難しいところはないですね(とてもめんどくさいですね・・・)。
ムーブ代入演算子も同じように実装できます。
template<typename T> struct enable_move_asign : check_copy_asign<T> { using base = check_copy_asign<T>; // ユーザー定義ムーブ代入演算子 enable_move_asign& operator=(enable_move_asign&& that) { this->asign_from(static_cast<base&&>(that)); } enable_move_asign() = default; enable_move_asign(const enable_move_asign&) = default; enable_move_asign(enable_move_asign&&) = default; enable_move_asign& operator=(const enable_move_asign&) = default; }; template<typename T> using check_move_asign = std::conditional_t< std::is_trivially_move_assignable_v<T>, check_copy_asign<T>, enable_move_asign<T> >; template<typename T> class my_optional : private check_move_asign<T> { public: // 他略 // ムーブ代入演算子 my_optional& operator=(my_optional&&) = default; };
やることは同じです。これによってほぼ全ての特殊メンバ関数のトリビアル性継承を実装することができました・・・
int main() { // 全てトリビアル static_assert(std::is_trivially_destructible_v<my_optional<int>>); static_assert(std::is_trivially_copy_constructible_v<my_optional<int>>); static_assert(std::is_trivially_move_constructible_v<my_optional<int>>); static_assert(std::is_trivially_copy_assignable_v<my_optional<int>>); static_assert(std::is_trivially_move_assignable_v<my_optional<int>>); // 全て非トリビアル static_assert(std::is_trivially_destructible_v<my_optional<std::string>> == false); static_assert(std::is_trivially_copy_constructible_v<my_optional<std::string>> == false); static_assert(std::is_trivially_move_constructible_v<my_optional<std::string>> == false); static_assert(std::is_trivially_copy_assignable_v<my_optional<std::string>> == false); static_assert(std::is_trivially_move_assignable_v<my_optional<std::string>> == false); // しかしユーザー定義されている static_assert(std::is_destructible_v<my_optional<std::string>>); static_assert(std::is_copy_constructible_v<my_optional<std::string>>); static_assert(std::is_move_constructible_v<my_optional<std::string>>); static_assert(std::is_copy_assignable_v<my_optional<std::string>>); static_assert(std::is_move_assignable_v<my_optional<std::string>>); }
確かに、トリビアル性を継承しつつそうでない場合はユーザー定義、というようになっています。
実はもう少し厳密にやると、そもそもT
がコピー可能でない場合に適切にdelete
するとかのハンドルが必要となりますが、主題ではないのでここではやりません。
このような複雑怪奇なテクニックはしかし、std::optional
やstd::variant
の実装で実際に使用されています。少なくともGCC/MSVCの実装はこうなっているはずです(MSVCは将来的に変更するかもしれませんが)。そして、std::expected
など類似のクラスでも同じ事をする必要が出てくるでしょう。
※ このあたりを書くにあたってはMSVCの実装(<optional>
と、xsmf_control.h
)を大変参考にしています。特に、xsmf_control.h
にはこのテクニックが一般化されまとまっていて、MSVCのoptional/variant
はどちらも同じものを使用しています。これはある程度TMPがわかればなんとか読めるので、気になった人はそちらを参照してください。
階層構造
check_xxxxx
みたいなエイリアステンプレートは、xxxxx
に対応する特殊メンバ関数がトリビアルでない場合にユーザー定義する層を挿入し、そうでないならスキップします。したがって、int
のような全トリビアルなクラスでは階層は最小になります。
my_optional<int>
optional_storage<int>
一方、std::string
のように全部トリビアルではないクラスではフルで挿入されることになります。
my_optional<std::string>
enable_move_asign<std::string>
enable_copy_asign<std::string>
enable_move_ctor<std::string>
enable_copy_ctor<std::string>
optional_storage<std::string>
例えばムーブだけトリビアルでないような型(move_non_trivial
)なら
my_optional<move_non_trivial>
enable_move_asign<move_non_trivial>
enable_move_ctor<move_non_trivial>
optional_storage<move_non_trivial>
のようなクラス階層になります。
一部のデバッガでは、このようなクラス階層を直接観測することができます(VSのデバッガだと多分途中が省略されるので見られない気がします)。あるいは観測してなんだこれ?と思ったことがあるかもしれません。
デフォルトコンストラクタ
optional
はその実装の都合上、デフォルトコンストラクタをトリビアルにすることができません。そのためoptional
以外を例にすると、次のように書くことで要素型のtrivially default constructible性を継承できます。
template<typename T> class wrap { T t; // 初期化しない public: wrap() = default; };
他のコンストラクタが存在するとデフォルトコンストラクタは暗黙delete
されるため、default
で書いておきます。この時、メンバに持っているT
のオブジェクトに対してデフォルトメンバ初期化してしまうとトリビアルにならないので注意が必要です。
int main() { static_assert(std::is_trivially_default_constructible_v<wrap<int>>); // パスする }
またおそらく、このような単純な型ではその他の部分のトリビアル性継承時にも先程までのような謎のテクニックを駆使する必要はないはずです。
C++20 Conditionally Trivial Special Member Functions
C++20ではコンセプトが導入され、それを利用したConditionally Trivial Special Member Functionsという機能が追加されました。これはまさに、ここまで見てきた事をコンセプトによって簡易に実現するための機能です。
これによって、my_optional
実装は次のようになります。
template<typename T> class my_optional { bool has_value = false; union { char dummy; T data; }; public: // デフォルトコンストラクタ constexpr my_optional() : has_value(false) , dummy{} {} // 値を受け取るコンストラクタ template<typename U=T> constexpr my_optional(U&& v) : has_value(true) , data(std::forward<U>(v)) {} // トリビアルに定義できるならそうする my_optional(const my_optional& that) requires std::is_trivially_copyable_v<T> = default; my_optional(my_optional&& that) requires std::is_trivially_movable_v<T> = default; my_optional& operator=(const my_optional& that) requires std::is_trivially_copy_assignable_v<T> = default; my_optional& operator=(my_optional&& that) requires std::is_trivially_move_assignable<T> = default; ~my_optional() requires std::is_trivially_destructible_v<T> = default; // そうでない場合はユーザー定義する my_optional(const my_optional& that) : has_value(that.has_value) , dummy{} { if (that.has_value) { new (&this->data) T(that.data); } } my_optional(my_optional&& that) : has_value(that.has_value) , dummy{} { if (that.has_value) { new (&this->data) T(std::move(that.data)); } } my_optional& operator=(const my_optional& that) { auto copy = that; *this = std::move(copy); return *this; } my_optional& operator=(my_optional&& that) { if (that.has_value) { if (this->has_value) { this->data = std::move(that.data); } else { new (&this->data) T(std::move(that.data)); } } else { this->reset(); } return *this; } ~my_optional() { this->reset(); } // reset()の定義も同様の記法で分岐できる void reset() requires std::is_trivially_destructible_v<T> { this->has_value = false; } void reset() { if (this->has_value) { this->data.~T(); } this->has_value = false; } };
default
な特殊メンバ関数に対してrequires
による制約を付加する事で、テンプレートパラメータの性質によって定義するしないを分岐することができ、100行以上も謎のコードを削減することができました・・・
ここでは、オーバーロード解決時の制約式による半順序に基づいて、特殊メンバ関数定義にも制約によって順序が付けられ、最も制約されている(かつそれを満たしている)1つだけが資格のある(eligible)特殊メンバ関数として定義され、それ以外はdelete
されます。
この場合、my_optional
のdefault
な特殊メンバ関数定義はis_trivially_~
によって制約されており、T
の対応する特殊メンバ関数がトリビアルである時my_optional
の対応する特殊メンバ関数もトリビアルな方が選択され、ユーザー定義のものは無制約なのでdelete
されます。逆に、T
の対応する特殊メンバ関数がトリビアルではない時、制約を満たさないことからdefault
のものがdelete
され、結果的に適切な一つだけが定義されています。
先ほどまで書いていたものすごく労力のかかった意味のわからないコードはこれによって不要になります。このConditionally Trivial Special Member Functionsという機能がいかに強力で素晴らしく、どれほどマイナーなのかがわかるでしょう!
そしてC++20以降、あのようなテクニックは忘れ去られていく事でしょう。この記事は、失われいく謎のテクニックを後世に伝えるとともに、理解しづらいConditionally Trivial Special Member Functionsという機能の解説を試みるものでした・・・
なぜにトリビアル?
長いので分けました。そもそもなんでそこまでしてトリビアル性にこだわるのか?という事を書いています。