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
/271
と 284
すべて、作成したい型を渡す必要があります。これは、それらが テンプレート であることを意味します 明示的なテンプレート パラメーターを渡す必要があります。ただし、テンプレート パラメーターを明示的に渡すと、特に次のような問題があります。
- 従属名がある場合は、
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
があります。 、 772
、 786
- 何 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 、 1225
と 1234
1241
の場合 1253
と同様に もちろん、ポリシーが空の状態を許可しない場合、これらはすべて静的に無効になります。
1268
も提供します と 1278
.後者は 1283
のようなものです バージョン。
結論
私の 1295
1308
と比較して、より柔軟で改良された亜種です .ポリシー ベースの設計により、ユーザーは 1 つの決定を強制するのではなく、亜種がどのように動作するかを選択できます。より詳細な制御が必要な場合は、1313
を簡単に使用できます 構成要素です。
この投稿は、私の通常の投稿よりもはるかに少ないコードを示しています。コードを見たい場合は、実装を見てください。関連するファイルは、taged_union.hpp、variant_impl.hpp、および variant.hpp です。 C ++ 11でどのように訪問する必要があるかを見てください。リターンタイプ控除なし。
他のすべてについては、type_safe をチェックしてください。それはさらに多くのことを行い、私のバリアントのドキュメントを見てください。