亜種に対する私の見解

C++17 は std::variant を追加する予定です。リンクされたドキュメントを引用すると、それは「型安全な共用体」です。A 07 11 のようなものです 、一度に 1 つのメンバーしか保存できません。これには多くの用途がありますが、悲しいことに、非自明な型とはうまく混ざりません。デストラクタを自分で呼び出す必要があります。さらに、ユニオン メンバーにアクセスすることを妨げるものは何もありません。はアクティブではありません。

24 それを修正します。アクティブなメンバーを切り替えるときにデストラクタを正しく呼び出し、無効なアクセスなどを防ぎます。しかし、私はそれに満足できず、今すぐ実装が必要でした。そのため、type_safe の一部として独自のバリアントを実装することにしました。

これは楽しい挑戦でした。前回の試みは 2 年前だったので、大幅に改善できました。私の設計上の決定事項をいくつか見ていきましょう。

ビルディング ブロック:37

43 の心臓部 タグ付きユニオンです。タグ付きユニオンは 56 のようなものです だけでなく、現在保存されているタイプも記憶しています。いくつかの 64 を保存します タイプの 1 つを一意に表します。

74 だけ コピー構築のような操作は、必要な型消去のためにいくらかのオーバーヘッドがあるため、別の 89 を作成することにしました。 C 97 と比較してオーバーヘッドがまったくないクラス - 107 に必要なスペースを除く タグ。

116 指定されたタイプのいずれかを格納するか、タイプを格納しません。コンストラクタはそれを空の状態にし、デストラクタは何もしません - クリーンアップはユーザーの責任であり、コピー/移動操作は削除されるため、誤って <を実行することはできませんコード>129 次の操作を実行できます:

    <リ>

    138 - ユニオンで指定されたタイプの新しいオブジェクトを作成します。

    <リ>

    143 - 指定されたタイプの現在保存されているオブジェクトを破棄します (タイプが一致する必要があります)。

    <リ>

    157 - 現在保存されている型の型識別子 (「タグ」) を返します。

    <リ>

    162 - 指定された型の格納された値を返します (型が一致する必要があります)。

このインターフェイスは非常にプリミティブですが、現在格納されている型を把握し、テンプレート パラメーターを渡す必要があります。これは、実装のオーバーヘッドがゼロであるため必要です。しかし、このインターフェイスはタイプ セーフでもあります。アクティブな型を切り替えることはできません。 C のように 173 .オブジェクトを配置または破棄するたびに、タグが自動的に更新され、182 タグをチェックするデバッグ アサーションがあります。

タグ自体 - 194 205 によって返されます 、218 への強力な typedef です。 、つまり、可変長型リスト内の現在アクティブな型のインデックス。比較のみを提供します。強力な typedef は 222 にも依存します。 type.これは、232 を比較できないことを意味します 異なる 244 からの s ID の一意性は型リストに依存するため、インスタンス化。

256 の実装 std::aligned_union のおかげで、それ自体は非常に簡単です。ただし、まだ解決しなければならない問題が 1 つあります。

265 /271284 すべて、作成したい型を渡す必要があります。これは、それらが テンプレート であることを意味します 明示的なテンプレート パラメーターを渡す必要があります。ただし、テンプレート パラメーターを明示的に渡すと、特に次のような問題があります。

  • 従属名がある場合は、297 が必要です 曖昧さ回避。私の言いたいことがわかるなら、同情します。
  • この投稿で概説した理由により嫌いです。

しかし、さらに大きな問題があります:

301 の値を取得するには 、次のようなコードを記述します:

tagged_union<int, float, char> u;
…
if (u.type() == type_id_for_int)
 do_sth_with_int(u.value<int>());

しかし、315 はどのように綴りますか? ?324 332 を提供できます 348 のコンストラクタを使用する方が直感的です。 .ただし、テンプレート パラメーターをコンストラクターに渡すことはできません!

幸いなことに、解決策があります。この問題をすべて解決する洗練された解決策です。上記でリンクした関数テンプレート パラメータの投稿で示したトリックを使用します。

秘訣は、テンプレートのインスタンス化を可能にするために使用するタグ タイプを作成することです:

template <typename T>
struct union_type {};

この小さな 350 すべての問題を解決します。これにより、360 の署名 、たとえば、次のようになります:

template <typename T>
void destroy(union_type<T>)
{
 … 
}

上記の例は次のようになります:

if (u.type() == union_t::type_id(union_type<int>{}))
 do_sth_with_int(u.value(union_type<int>{}));

379 に関するすべての詳細を見つけることができます

構成要素:訪問

380 の使用 たとえば、現在保存されている 397 の型を破棄したいとします。 :

if (u.type() == union_t::type_id(union_type<int>{}))
 u.destroy(union_type<int>{});
else if (u.type() == union_t::type_id(union_type<float>{}))
 u.destroy(union_type<float>{});
else if (u.type() == union_t::type_id(union_type<char>{}))
 u.destroy(union_type<char>{});
else
 // no value stored - or maybe I forgot a type?

どの型が格納されているか静的にわからない場合は常に、この種の型スイッチが必要になります。冗長でエラーが発生しやすいです。

それでは、一般的な方法で一度実装してみましょう。

type_safe のいくつかの型は (非メンバー) 406 を提供します function.It は、オブジェクトとファンクターを受け取り、格納された/基になる型の何らかの形式でそれを呼び出します。417 の場合 、 423 次のようになります:

template <typename ... Types, typename Func, typename ... Args>
void with(tagged_union<Types>& u, Func&& f, Args&&... additional_args);

// also overloads for `const&`, `&&` and `const&&`.

基本的に 436 を呼び出します 、ここで 446 現在共用体に格納されている型です。呼び出しが整形式でないか、型が格納されていない場合は、459

469 で - すみません - 470 を実装できます 型を静的に認識せずに破棄する関数:

template <typename ... Types>
void destroy(tagged_union<Types...>& u)
{
 with(u, [&](auto& value)
 {
 // we don't actually need the stored object
 // remember, never called if no object stored
 using type = std::decay_t<decltype(value)>;
 u.destroy(union_type<T>{});
 });
}

488 を実装することもできます 、 493 で使用されます s コピー コンストラクター:

template <typename ... Types>
void copy(tagged_union<Types...>& dest, const tagged_union<Types...>& other)
{
 // assume dest is empty
 with(other, [&](const auto& value)
 {
 using type = std::decay_t<decltype(value)>;
 dest.emplace(union_type<T>{}, value);
 });
}

506 格納された型が静的に認識されず、非常にエレガントに処理できるようになるたびに必要になります。

515 問題

522 538 の基本的な実装と設計の問題を回避するように、非常に慎重に作成されています。 s:例外の安全性.549 以前の値が破棄されている必要があります 553 宛先が空である必要があります。

568 を考えてみましょう タイプ 579 のオブジェクトを含む タイプ 582 の新しいオブジェクトに変更したい .

次の 2 つのことを行う必要があります:

<オール> <リ>

タイプ 594 のオブジェクトを破棄します .

<リ>

タイプ 608 の新しいオブジェクトを作成します

新しいものを作成する前に破棄する必要がありますが、 610 のコンストラクターが 例外をスローしますか?その後、バリアントにはオブジェクトが含まれなくなり、強力な例外安全性が提供されず、628 がさらに防止されます。 常に値が含まれます。

しかし、新しい 635 を作成するためにテンポラリを使用すると オブジェクトを移動してから移動しますか?これはうまくいく可能性があります:

<オール> <リ>

一時的な 640 を作成します オブジェクト。

<リ>

タイプ 655 のオブジェクトを破棄します .

<リ>

一時的な 664 を移動します ユニオンストレージに。

これにより、move コンストラクターがスローしない限り、強力な例外安全性が提供されます。この場合、以前と同じ問題が発生します。

しかし、おそらく常に 1 つの型が非スローのデフォルト構成可能であるバリアント (フォールバック) である場合、これを行うことができます:

<オール> <リ>

タイプ 670 のオブジェクトを破棄します .

<リ>

タイプ 686 の新しいオブジェクトを作成します

<リ>

2) がスローされる場合、バリアントでフォールバック タイプのオブジェクトを作成します。

これはまだ強力な例外安全性を提供しませんが、少なくとも 690 空にはなりません。

しかし、決して空にならないバリアントの保証を犠牲にしましょう。 /コード> 、またはそうではありません。唯一の違いは:718 多くのタイプ 723 のいずれかを格納できます 1 つだけです。したがって、インターフェイスで空の状態を受け入れるだけです。

これは私のお気に入りのソリューションですが、多くの人にはうまくいきません。いくつかの追加のトリックがありますが、追加のストレージとオーバーヘッドが必要です。それが 738 の理由です。 空の状態は「無効」であり、たとえば、上記の「create-with-temporary」アルゴリズムのムーブ コンストラクターがスローしたときに発生します。

では、より良い解決策は何でしょうか?

まあ、それはバリアントの使用法に依存します.空にならないことを保証したい場合もあれば、投げない移動コンストラクタを提供できる場合もあります.フォールバック型がある場合もあれば、標準的なセマンティクスが必要な場合もあります.

だから私のバリアントは 740 です .ポリシー ベースの設計を使用して、この動作をカスタマイズします。バリアント ポリシーは次の 2 つのことのみを制御します:

    <リ>

    バリアントが「受け入れられた」空の状態を持っているかどうか、または空が単なる無効な状態であるかどうか

    <リ>

    759 動作、つまり、型を変更する必要があるときに何をすべきか

また、上で説明したアルゴリズムも実装しました。762 があります。 、 772786 - 何 790 する - そして 805 また、便利な typedef も提供します:813 、最初のタイプはフォールバックと 823 です .835 848 を使用 851 の模倣 最初のタイプが 865 でない限り 、その場合は 873 を使用します .

ここでのポリシーベースの設計は本当に効果があります。

885 インターフェースデザイン

しかし 899 のインターフェースは 908 とは大きく異なります そして - 私が主張する - より良い.

まず、すべてのアクセス関数はメンバー関数です。914 のように 、タグタイプを使用 - 924 、これは 930 の単なるエイリアスです .これは 944 のようなものです std::in_place_type_t で行いますが、インターフェイス全体で一貫しています。

955 で見たように 、バリアントに型が含まれているかどうかを照会してから、それに対して何かを行うのは非常に面倒です:

if (u.type() == union_t::type_id(union_type<int>{}))
 do_sth_with_int(u.value(union_type<int>{}));

これは 967 でも機能します 、ただし、975 を作成するには、ネストされた typedef にアクセスする必要があります .最初の単純化は 981 を提供します 関数:

if (variant.has_value(variant_type<int>{})
 do_sth_with_int(variant.value(variant_type<int>{}));

しかし、 993 のようなより高度な機能があります :

do_sth_with_int(variant.value_or(variant_type<int>{}, fallback_value));

上で述べたように、1003 1012 だけです :タイプ 1023 の値があるか またはありません。したがって、1038 を取得することもできます 1049 から .An 1051 正確に言うと、これは 1066 へのオプションの参照です。 .そうです、オプションの参照であり、ポインターではありません.While 1079 基本的には、マイナーな最適化レベルの後のポインターであり、すべての高度なオプション機能も提供します。

1089 を使用するだけです 必要なすべての安全なアクセス機能を利用できます。

1092 1103 よりもはるかに優れたソリューションです の 1113 .

1122 メンバー関数 1136 も提供します .1149 新しい 1158 を返します 1165 の結果が含まれます または 1171 、それが不正な形式の場合。これにより、 1182 の変換が可能になります .

1196 に注意してください 可能な空の状態を完全に受け入れます。1201 とは異なり、そこに配置するデフォルトのコンストラクターがあります。 デフォルトで最初のタイプ、special 1212 を構築する s 、 12251234 1241 の場合 1253 と同様に もちろん、ポリシーが空の状態を許可しない場合、これらはすべて静的に無効になります。

1268 も提供します と 1278 .後者は 1283 のようなものです バージョン。

結論

私の 1295 1308 と比較して、より柔軟で改良された亜種です .ポリシー ベースの設計により、ユーザーは 1 つの決定を強制するのではなく、亜種がどのように動作するかを選択できます。より詳細な制御が必要な場合は、1313 を簡単に使用できます 構成要素です。

この投稿は、私の通常の投稿よりもはるかに少ないコードを示しています。コードを見たい場合は、実装を見てください。関連するファイルは、taged_union.hpp、variant_impl.hpp、および variant.hpp です。 C ++ 11でどのように訪問する必要があるかを見てください。リターンタイプ控除なし。

他のすべてについては、type_safe をチェックしてください。それはさらに多くのことを行い、私のバリアントのドキュメントを見てください。