Eggs.Variant-パートI

この翻訳を公開するために、 @ encyclopedistの最近の記事「ファクトリーメソッド(動的メモリ割り当てなし)」 に対するコメント動機付けられました。 この記事には興味がありましたが、簡単なグーグル検索では翻訳が明らかになりませんでした。 「無秩序。」と思った。「C ++に関するこのような興味深い記事はロシア語に翻訳されていない。 修正する必要があります。」



目次

  1. はじめに
  2. 設計
  3. 実装



  4. まだ言われていないこと




Eggs.Variantの開発に関する考察C ++ 11/14の一般的なタイプセーフマークアップ結合



はじめに



ユニオンは、非静的メンバーの1つだけが一度に格納できる特別な種類のクラスです。 最大のメンバーを収容するために必要なスペースを占有します。

9 [class] / 5 Unionは、 union



キーワードで定義されたクラスです。 同時に、彼はメンバーの1人(9.5)のみを保持できます。 [...]

9.5 [class.union] / 1ユニオンでアクティブにできる非静的メンバーは1つだけです。つまり、ユニオン内の特定の時点では、その非静的メンバーの1つだけを格納できます。 [...]プールのサイズは、その最大の非静的メンバーを収容するのに十分です。 各非静的メンバーは、あたかも構造体の唯一のメンバーであるかのようにメモリに保存されます。 ユニオンオブジェクトの非静的メンバーはすべて同じアドレスを持ちます。



オリジナル
9 [クラス] / 5ユニオンは、クラスキーユニオンで定義されたクラスです。 一度に最大1つのデータメンバーを保持します(9.5)。 [...]

9.5 [class.union] / 1ユニオンでは、最大で1つの非静的データメンバーをいつでもアクティブにできます。つまり、最大で1つの非静的データメンバーの値をいつでも組合。 [...]ユニオンのサイズは、最大の非静的データメンバーを含めるのに十分です。 各非静的データメンバーは、構造体の唯一のメンバーであるかのように割り当てられます。 ユニオンオブジェクトのすべての非静的データメンバーは同じアドレスを持ちます。











C ++ 98では、共用体メンバーは単純なオブジェクト型に制限されています。 これらのタイプの場合、それらのライフタイムは、 リポジトリが受信されると始まり、再利用またはリリースされると終了します。

3.8 [basic.life] / 1 [...]タイプT



オブジェクトのライフタイムは、次の場合に始まります。

  • タイプT



    アライメントとサイズを持つリポジトリが取得され、
  • オブジェクトの初期化は、それが重要な場合、完了します。


タイプT



オブジェクトのライフタイムは、次の場合に終了します。

  • T



    が非自明なデストラクタ(12.4)を持つクラス型の場合、デストラクタの呼び出しが開始されます。または
  • オブジェクトが占有しているストレージが再利用または解放されました。




オリジナル
3.8 [basic.life] / 1 [...]タイプT



オブジェクトのライフタイムは、次の場合に始まります。

  • タイプT



    の適切な配置とサイズのストレージが取得されます。
  • オブジェクトに重要な初期化がある場合、その初期化は完了します。


タイプT



オブジェクトのライフタイムは、次の場合に終了します。

  • T



    が非自明なデストラクタ(12.4)を持つクラス型の場合、デストラクタ呼び出しが開始されます。または
  • オブジェクトが占有しているストレージは再利用または解放されます。






この特別な保証により、新しい値をアソシエーションに割り当てるだけでアソシエーションのアクティブなメンバーを変更でき、ストレージを効果的に再利用できるようになります。これは、文字ではなく、標準の精神でうまく一致しています。

さらに、アソシエーションはどのメンバーがアクティブであるかを認識しないため、この情報を知らずに特別なメンバー機能を実装する必要があります。 ユニオンメンバは単純な型に制限されているため、アクティブなメンバとは無関係に、ユニオンの基礎となるバイトに関して特別なメンバ関数を実装できます。

9.5 [class.union] / 1 [...]非自明なコンストラクター(12.1)、非自明なコピーコンストラクター(12.8)、非自明なデストラクター(12.4)、または非自明なコピー代入演算子( 13.5.3、12.8)を持つクラスオブジェクトはメンバーになれませんユニオン、またはユニオン内にある配列要素。 [...]



オリジナル
9.5 [class.union] / 1 [...]非自明なコンストラクター(12.1)、非自明でないコピーコンストラクター(12.8)、非自明なデストラクター(12.4)、または非-自明なコピー代入演算子(13.5.3、12.8)は、ユニオンのメンバーにすることも、そのようなオブジェクトの配列にすることもできません。 [...]





C ++ 11では、この制限は削除されました。 ユニオンメンバは、どのタイプでも使用できます。 重要なメンバーを切り替えるには、現在のアクティブなメンバーを明示的に破棄し、 検索演算子new



を使用して新しいアクティブなメンバーを作成する必要があります。

9.5 [class.union] / 4 [注:一般的なケースでは、デストラクタの明示的な呼び出しを1回、ホスト演算子new



明示的な呼び出しを1回使用して、列挙のアクティブなメンバーを変更する必要があります。 -注の終わり] [例:タイプM



非静的メンバーm



およびタイプN



n



持つ列挙タイプU



を持つオブジェクトu



を考えますN



M



に非自明なデストラクタがあり、 N



に非自明なコンストラクタがある場合(たとえば、それらが宣言または仮想関数を継承する場合)、次のデストラクタの使用とnew



演算子を使用して、アクティブメンバu



m



からn



安全に変更できます。

 um~M(); new (&u.n) N;
      
      





-例の終わり]



オリジナル
9.5 [class.union] / 4 [注:一般的に、ユニオンのアクティブなメンバーを変更するには、明示的なデストラクター呼び出しと新しい演算子の配置を使用する必要があります。 –注:[例:タイプM



非静的データメンバーm



とタイプN



n



を含むユニオンタイプU



オブジェクトu



を考えますN



M



に非自明なデストラクタがあり、 N



に非自明なコンストラクタがある場合(たとえば、仮想関数を宣言または継承する場合)、デストラクタと配置new演算子を使用して、 u



のアクティブメンバをm



からn



安全に切り替えることができます次のとおりです。

 um~M(); new (&u.n) N;
      
      





—終わりの例]





関連付けのメンバーのいずれかの特別なメンバー関数が自明でない場合、関連付け自体の特別なメンバー関数は、ユーザー自身によって提供されない限り、暗黙的に削除済みとして定義されます。

9.5 [class.union] / 2 [注:ユニオンの非静的メンバーに非自明なデフォルトコンストラクター(12.1)、コンストラクターのコピー(12.8)、コンストラクターの移動(12.8)、割り当て演算子のコピー(12.8)、割り当て演算子の移動(12.8)がある場合またはデストラクタ(12.4)の場合、対応する組合のメンバー関数はユーザーが提供する必要があります。そうしないと、暗黙的に組合から削除されます(8.4.3)。 —注を終了]

9.5 [class.union] / 3 [例:次のユニオンを検討します。

 union U { int i; float f; std::string s; };
      
      





std::string



type(21.3)はすべての特別なメンバー関数の非自明なバージョンを宣言するため、デフォルトのコンストラクター、コピー/移動コンストラクター、コピー/移動代入演算子、およびデストラクターはU



型から暗黙的に削除されますU



タイプU



を使用するには、これらのメンバー関数の一部をユーザーが提供する必要があります。 -例の終わり]



オリジナル
9.5 [class.union] / 2 [注:ユニオンの非静的データメンバーに非自明なデフォルトコンストラクター(12.1)、コピーコンストラクター(12.8)、コンストラクターの移動(12.8)、割り当て演算子のコピー(12.8)がある場合、移動代入演算子(12.8)、またはデストラクター(12.4)の場合、ユニオンの対応するメンバー関数はユーザーが指定する必要があります。そうしないと、ユニオンの暗黙的に削除されます(8.4.3)。 —注を終了]

9.5 [class.union] / 3 [例:次のユニオンを検討します。

 union U { int i; float f; std::string s; };
      
      





std::string



(21.3)はすべての特別なメンバー関数の自明でないバージョンを宣言するため、 U



には暗黙的に削除されたデフォルトコンストラクター、コンストラクターのコピー/移動、代入演算子のコピー/移動、およびデストラクターがあります。 U



を使用するには、これらのメンバー関数の一部またはすべてをユーザーが提供する必要があります。 —終わりの例]





これらの非自明なメンバー関数は、列挙のアクティブなメンバーが渡されるように既知である場合にのみ-通常のセマンティクスに従って提供できます。 マークされた共用体は、 それ自体の知識を持つ共用体に類似した共用体またはクラスです 。つまり、現在アクティブなメンバーがあればそれを知るための識別子が含まれています。 マークされたユニオンは、その自明性に関係なく、すべての特別なメンバー関数を提供できます。

eggs::variant<Ts...>



クラスのインスタンスは、 eggs::variant<Ts...>



オブジェクトのタグ付き結合です 。 アクティブなメンバーを切り替えるための自然なインターフェイスを提供し、すべての特別なメンバー関数に通常のセマンティクスを提供します。

 eggs::variants<N, M> u; // u     u = M{}; //    u   M u = N{}; //    u   N,      //    - using U = eggs::variants<int, float, std::string>;
      
      







設計



設計の究極の目標は、機能を損なうことなく、マークされたユニオンの設計を一般化し、改善することです。 つまり、アソシエーションの現在のアクティブなメンバーを選択するための特別なメンバーが存在しないようにするか、スペースをほとんど占有しないようにします。

 struct U { union { T0 m0; ...; TN mN; }; std::size_t which; } u;
      
      





これに置き換えられます:

 using V = eggs::variant<T0, ..., TN>; V v;
      
      





特に:



基本的に、インターフェースは、メインライブラリの技術仕様で定義されているstd::experimental::optional<T>



に基づいていますoptional<T>



概念モデルは、 nullopt_t



型とT



型のラベル付きユニオンで構成されます。 optional<T>



に対して行われた設計決定は、 variant<Ts...>



に簡単に転送できます。その概念モデルは、 nullvariant_t



型とTs



背後に隠された型のラベル付きユニオンで構成されます。 すべての特別なメンバー関数と関連する演算子のセマンティクス、およびアクティブなメンバーを切り替えるためのインターフェイス(構築、割り当て、または配置)は、 optional<T>



から借用されoptional<T>





アクティブなメンバーへのアクセスは、 std::function



スタイルで行われstd::function



。これは、ターゲットの正しいタイプ(貧しい人々のdynamic_cast



など)で要求された場合、ターゲットへのポインターを返します。 さらに、アクティブなメンバー(存在する場合)へのNULLポインターを取得することもできます。これは、補助関数の実装を簡素化するのに非常に役立つことが判明しました。

最後に、 std::tuple



などのヘルパークラスstd::tuple



提供され、実行時のキャストのセマンティクスに近い非自明なセマンティクスがありますが、インデックスまたはタイプによる要素へのアクセスstd::tuple



提供されます。

参照ドキュメントはこちらにあります



実装



variant<Ts>



直接実装では、基盤となる緩和されたユニオンをストレージとして使用します。

 template <typename ...Ts> union storage { nullvariant_t nullvariant; Ts... members; };
      
      





ただし、上記のコードは、 C ++構文の観点からは正しくありません。テンプレートパラメータのセットはこのコンテキストでは展開できないためです。参照できるようにメンバー名を含める必要があります。 代わりに、再帰的なアプローチを使用して、基礎となるストレージを構築します。

 template <typename ...Ts> union storage; template <typename T, typename ...Ts> union storage<T, Ts...> { nullvariant_t nullvariant; T head; storage<Ts...> tail; }; template <> union storage<> { nullvariant_t nullvariant; };
      
      





残念ながら、これはTs



型の重要な特殊メンバー関数が結果としてリポジトリから対応する特殊メンバー関数を削除することを考えると、それほど単純ではありません。 この実装を使用するには、少なくともデフォルトのコンストラクタとデストラクタを結果のリストに実装する必要がありますが、デストラクタは有用なことは何もできません。

緩和された関連付けがC ++に登場する前に使用された最も単純な実装は、 Ts



任意の型の格納に適したベアストレージを使用します-注意、スポイラー:場合によっては、動作しません。 標準には、作業を容易にする特別なプロパティも用意されています。

10.20.7.6 [meta.trans.other]

 template <std::size_t Len, class... Types> struct aligned_union;
      
      





  • 条件:少なくとも1つのタイプを指定する必要があります。
  • コメント:メンバータイプごとのtypedefはPODタイプである必要があり、 Types



    リストにリストされているオブジェクトの初期化されていないストレージとして使用できる。 そのサイズは少なくともLen



    でなければなりません。 静的メンバーalignment_value



    は、 std::size_t



    型の整数定数である必要があり、その値は、 Types



    リストにリストされているすべての型の最も厳密な配置を定義します。




オリジナル
10.20.7.6 [meta.trans.other]

 template <std::size_t Len, class... Types> struct aligned_union;
      
      





  • 条件:少なくとも1つのタイプが提供されます。
  • コメント:メンバーtypedefタイプは、タイプがTypes



    リストされているオブジェクトの初期化されていないストレージとしての使用に適したPODタイプでなければなりません。 そのサイズは少なくともLen



    でなければなりません。 静的メンバーalignment_value



    は、 std::size_t



    型の整数定数であり、その値はTypes



    リストされているすべての型の中で最も厳密なアライメントです。






このプロパティは、緩和されたアソシエーションの出現とともに、 作業ドラフトから既に削除されており、現在では陳腐化の潜在的な候補であることに注意する必要があります。 C ++ 14で可能な代替は次のとおりです。

 template <std::size_t Len, typename ...Types> struct aligned_union { static constexpr std::size_t alignment_value = std::max({alignof(Types)...}); struct type { alignas(alignment_value) unsigned char _[std::max({Len, sizeof(Types)...})]; }; };
      
      





記憶域の型としてaligned_union



を使用すると、 variant<Ts>



簡易バージョンを次のように実装できます。

 template <typename ...Ts> class variant { template <typename T> struct _index_of { /*...*/ }; //  T  Ts...,   0 public: static constexpr std::size_t npos = std::size_t(-1); variant() noexcept : _which{npos} {} template <typename T> variant(T const& v) : _which{_index_of<T>::value} { new (target<T>()) T(v); //   T     //   new } /*...*/ std::size_t which() const noexcept { return _which; } template <typename T> T* target() noexcept { return _which == _index_of<T>::value ? static_cast<T*>(static_cast<void*>(&_storage)) : nullptr; } template <typename T> T const* target() const noexcept { return _which == _index_of<T>::value ? static_cast<T const*>(static_cast<void const*>(&_storage)) : nullptr; } private: std::size_t _which; typename std::aligned_union<0, Ts...>::type _storage; };
      
      





特別なメンバー関数は、アクティブなメンバー(存在する場合)にリダイレクトする必要があります。 繰り返しになりますが、この目標を達成するためにswitch



を使用することはできません-これを行うことができれば、それははるかに簡単ではありませんが-即時置換には再帰的な実装があります:

 struct _destructor { template <typename T> static void call(void* ptr) { static_cast<T*>(ptr)->~T(); } }; variant<Ts...>::~variant() { apply<_destructor, Ts...>(_which, &_storage); } template <typename F> void apply(std::size_t /*which*/, void* /*storage*/) {} template <typename F, typename T, typename ...Ts> void apply(std::size_t which, void* storage) { if (which == 0) { F::template call<T>(storage); } else { apply<F, Ts...>(which - 1, storage); } }
      
      





apply



メソッドの非再帰的な実装は、 switch



が行う方法と同様に、ジャンプテーブルを使用して構築でき、その後に適切なエントリへの遷移が続きます。

 template <typename F, typename ...Ts> void apply(std::size_t which, void* storage) { using fun_ptr = void(*)(void*); static constexpr fun_ptr table[] = {&F::template call<Ts>...}; if (which < sizeof...(Ts)) { table[which](storage); } }
      
      





非再帰的実装は、生成されるコードを高速化するだけでなく、 Ts



リストに多数の型がある場合にコンパイルを大幅に高速化します-あなたの場合、推定値は変わる可能性があります。



簡単にコピーされたタイプ



簡単にコピーされるタイプとは、その構成ビットをコピーすることで、つまりstd::memcpy



を使用してコピーできるタイプです。

3.9 [basic.types] / 2簡単にコピーされた型T



オブジェクト(基本クラスのサブオブジェクトを除く)に対して、型T



有効な値が含まれているかどうかにかかわらず、その構成バイト(1.7)をchar



またはunsigned char



配列にコピーできます。 char



またはunsigned char



配列の内容がオブジェクトにコピーされた場合、結果としてオブジェクトは元の値を取得するはずです。 [...]

3.9 [basic.types] / 3 T



2つのポインターobj2



タイプT



異なるオブジェクトobj1



およびobj2



指す場合、任意の簡単にコピーされたタイプT



について、 obj1



またはobj2



いずれかが基本クラスのサブオブジェクトであり、 obj2



バイト(1.7)がobj2



コピーされます、結果として、 obj2



obj1



と同じ値が含まれている必要があります。 [...]



オリジナル
3.9 [basic.types] / 2簡単にコピー可能な型T



オブジェクト(ベースクラスサブオブジェクト以外)の場合、オブジェクトが型T



有効な値を保持しているかどうかにかかわらず、オブジェクトを構成する基本バイト(1.7) char



またはunsigned char



配列にコピーされます。 char



またはunsigned char



配列の内容がオブジェクトにコピーされた場合、オブジェクトは元の値を保持します。 [...]

3.9 [basic.types] / 3 T



への2つのポインターが別個のT



オブジェクトobj1



obj2



を指している場合、任意の簡単にコピー可能な型T



に対して、 obj1



obj2



もベースクラスのサブオブジェクトではなく、基礎となるバイト(1.7) obj1



obj2



にコピーされ、その後、 obj2



obj1



と同じ値を保持します。 [...]





簡単にコピーされたメンバーの和集合自体は簡単にコピーされ、追加の最適化の候補に入れられます。これは、簡単にコピーされたタイプでは実行できません。 簡単にコピーされた型を持つvariant



、簡単にコピーされる傾向があるはずです。

9 [クラス] / 6簡単にコピーされたクラスは、次のクラスです:

  • 自明でないコピーコンストラクターはありません(12.8)、
  • 非自明な変位コンストラクターはありません(12.8)、
  • 自明でないコピー割り当て演算子(13.5.3、12.8)、
  • 自明な移動代入演算子(13.5.3、12.8)がなく、
  • 些細なデストラクタ(12.4)があります。


[...]



オリジナル
9 [クラス] / 6簡単にコピー可能なクラスは、次のクラスです:

  • 自明でないコピーコンストラクタはありません(12.8)、
  • 非自明な移動コンストラクターはありません(12.8)、
  • 自明でないコピー割り当て演算子はありません(13.5.3、12.8)、
  • 自明でない移動割り当て演算子はありません(13.5.3、12.8)。
  • 些細なデストラクタ(12.4)があります。


[...]





これを実現するには、Tのすべてのタイプが簡単にコピーされる場合、別のvariant



特殊化を選択する必要があります。 上記の特別なメンバー関数は、この特殊化のためにユーザーによって提供されるべきではありません-それらは、デフォルトで生成されるように、それらが最初に定義されるときに暗黙的または明示的に指定されるべきです。 実装は、簡単な暗黙の定義を提供します。 コピーおよび移動演算子は、それらを含むストレージビットをディスクリミネーターと一緒に単にコピーしますが、デストラクタは何もしません。

ただし、1つ問題があります。簡単にコピーされたクラスはまったくコピーできない場合がありますが、最初に定義されたときに削除された特別なメンバー関数は簡単です。 たとえば、 boost::noncopyable



クラスの定義方法を見てください。

 class noncopyable { protected: constexpr noncopyable() = default; noncopyable(noncopyable const&) = delete; noncopyable& operator=(noncopyable const&) = delete; ~noncopyable() = default; };
      
      





std::is_trivially_copyable



noncopyable



クラスに対してtrue



を出力するのは驚きです。 さらに大きな驚きは、コピーできないリモートメンバ関数がまったく使用されていないため、 variant<noncopyable>



インスタンスを正常にコピーできることです。 実際、これは型セキュリティ違反は、型指定されていないベアストレージを使用してアクティブなメンバーを格納するという決定に起因します。



簡単に破壊可能な型



型のもう1つの重要なカテゴリは、 簡単に破壊可能型です。

12.4 [class.dtor] / 5 [...]デストラクタは、ユーザーから提供されない場合、および次の場合に簡単です。

  • それは仮想ではなく、
  • 特定のクラスのすべての直接の基底クラスには、簡単なデストラクタがあり、
  • 型のクラス(またはクラスの配列)を持つこのクラスのすべての非静的メンバーの場合、そのような各クラスには簡単なデストラクタがあります。


それ以外の場合、デストラクタは重要です。



オリジナル
12.4 [class.dtor] / 5 [...]デストラクタは、ユーザーが提供していない場合、および次の場合に簡単です。

  • デストラクタは仮想ではなく、
  • そのクラスのすべての直接基底クラスには簡単なデストラクタがあり、
  • クラス型(またはその配列)であるそのクラスのすべての非静的データメンバについて、そのような各クラスには簡単なデストラクタがあります。


それ以外の場合、デストラクタは重要です。





constexpr



式のコンテキストでインスタンスを使用できるリテラル型の要件の1つは、その単純な破壊性であるため、これは特に重要です。

3.9 [basic.types] / 10次の場合、型はリテラル型です。

  • [...]
  • ( 9), :

    • ,
    • (8.5.1) constexpr



      - ,
    • .






オリジナル
3.9 [basic.types] / 10次の場合、型はリテラル型です。

  • [...]
  • 次のすべてのプロパティを持つクラス型(9項):

    • 取るに足らないデストラクタがあり、
    • 集約型(8.5.1)であるか、少なくとも1つのconstexprコンストラクターまたはコピーまたは移動コンストラクターではないコンストラクターテンプレートがあります。
    • すべての非静的データメンバーと基本クラスは、不揮発性リテラルタイプです。








少なくとも1つのメンバーがリテラル型であり、残りのメンバーが簡単に破壊できる場合、ユニオンはリテラル型にすることができます。したがってvariant



、これらの条件下では、リテラル型になるように努力する必要があります。variant



すべてのタイプTs



が自明に破壊可能である場合は、別の専門分野を選択する必要がありますただし、定数式の制限にはvoid



、オブジェクトへのポインタへのポインタのキャストに関する制限があるため、あまり有用ではありません

5.19 [expr.const] / 2次の式のいずれかで抽象マシン(1.9)のルールに従ったe



計算e



が計算できない場合にのみ、条件式は定数式のコアになります。

  • [...]
  • cv void*



    オブジェクトへのポインターの型から型への変換
  • [...]




オリジナル
5.19 [expr.const]/2 A conditional-expression e



is a core constant expression unless the evaluation of e



, following the rules of the abstract machine (1.9), would evaluate one of the following expressions:

  • [...]
  • a conversion from type cv void*



    to a pointer-to-object type;
  • [...]






また、型なしのrawストレージを使用するという決定により、汎用ラベル付きプールの実装の完全性は、手動で実装することで達成できるレベルと同じレベルに制限されます。



まだ言われていないこと



生のストレージに基づいた実装は、その価格を正当化しますが、水を保持しませんが、通常のフレームワークを超える価値があります。一般化されたタイプセーフのマークアップされたユニオンでの値の保存は、通常のユニオンでなければなりません。そうでない場合、すべての機能を使用しても機能しません。



All Articles