オーバーロード パターンで std::variant を参照する

通常、464 のオーバーロード パターンを使用します。 . 477 タイプセーフ共用体です。 A488 498 (C++17) は、その型の 1 つから 1 つの値を持ちます。507 ビジターを適用できます。まさにここで、非常に便利なオーバーロード パターンが登場します。

前回の投稿「パラメーター パックとフォールド式を使用したスマート トリック」で、ラムダを使用してオーバーロード セットを作成するためのスマートなトリックとして、オーバーロード パターンを紹介しました。通常、オーバーロード パターンは、512<が保持する値にアクセスするために使用されます。 /コード> .

ほとんどの開発者が 525 を知らないことは、私の C++ セミナーで知っています。 そして533 それでもユニオンを使用します。そこで、540 について簡単に思い出させてください。 および 557 .

567

std::variant は型安全な共用体です。 std::variant のインスタンスには、その型の 1 つからの値があります。値は、参照、C 配列、または void であってはなりません。 std::variant は、1 つの型を複数回持つことができます。デフォルトで初期化された std::variant は、最初の型で初期化されます。この場合、最初の型にはデフォルトのコンストラクターが必要です。 cppreference.com に基づいた例を次に示します。

// variant.cpp

#include <variant>
#include <string>
 
int main(){

 std::variant<int, float> v, w;
 v = 12; // (1)
 int i = std::get<int>(v);
 w = std::get<int>(v);  // (2)
 w = std::get<0>(v); // (3)
 w = v; // (4)
 
 // std::get<double>(v); // (5) ERROR
 // std::get<3>(v); // (6) ERROR
 
 try{
 std::get<float>(w); // (7)
 }
 catch (std::bad_variant_access&) {}
 
 std::variant<std::string> v("abc"); // (8)
 v = "def"; // (9)

}

バリアント v と w の両方を定義します。これらは int 値と float 値を持つことができます。初期値は 0 です。v は 12 になります (1 行目)。 std::get(v) は値を返します。行 (2) - (3) では、バリアント v をバリアント w に割り当てる 3 つの可能性が示されています。ただし、いくつかのルールを覚えておく必要があります。タイプ別 (5 行目) またはインデックス別 (6 行目) にバリアントの値を要求できます。タイプは一意で、インデックスは有効でなければなりません。 7 行目では、バリアント w が int 値を保持しています。したがって、std::bad_variant_access 例外が発生します。コンストラクター呼び出しまたは代入呼び出しが明確な場合、単純な変換が行われます。これが、行 (8) で C 文字列を使用して std::variant を作成したり、バリアントに新しい C 文字列を割り当てたりできる理由です (行 9)。

もちろん、571 についてはまだまだあります Bartlomiej Filipek による投稿「C++17 の std::variant について知っておくべきことすべて」をお読みください。

関数 586 のおかげで 、C++17 は 597 の要素を訪問する便利な方法を提供します .

600

古典的なデザイン パターンによるとビジター パターンのように聞こえるものは、実際にはバリアントのコンテナーの一種のビジターです。

std::visit を使用すると、ビジターをバリアントのコンテナーに適用できます。ビジターは呼び出し可能でなければなりません。 callable は、呼び出すことができるものです。典型的な callable は、関数、関数オブジェクト、またはラムダです。私の例ではラムダを使用しています。

// visitVariants.cpp

#include <iostream>
#include <vector>
#include <typeinfo>
#include <variant>

 
int main(){
 
 std::cout << '\n';
 
 std::vector<std::variant<char, long, float, int, double, long long>> // 1
 vecVariant = {5, '2', 5.4, 100ll, 2011l, 3.5f, 2017};
 
 for (auto& v: vecVariant){
 std::visit([](auto arg){std::cout << arg << " ";}, v); // 2
 }
 
 std::cout << '\n';
 
 for (auto& v: vecVariant){
 std::visit([](auto arg){std::cout << typeid(arg).name() << " ";}, v); // 3
 }
 
 std::cout << "\n\n";
 
}

(1) バリアントの std::vector を作成し、各バリアントを初期化します。各バリアントは、char、long、float、int、double、または long long 値を保持できます。バリアントのベクトルをトラバースしてラムダを適用するのは非常に簡単です (行 (2) と (3))。最初に現在の値を表示し (2)、次に typeid(arg).name( の呼び出しのおかげで) ) (3)、現在の値の型の文字列表現を取得します。

罰金?いいえ!。プログラム 612 で使用しました 一般的なラムダ。その結果、型の文字列表現は、gcc を使用してかなり判読できません:"626 ". 正直なところ、特定のラムダをバリアントの各タイプに適用したいと思っています。今、オーバーロード パターンが私の助けになります。

過負荷パターン

オーバーロード パターンのおかげで、各型を読み取り可能な文字列で表示し、各値を適切な方法で表示できます。

// visitVariantsOverloadPattern.cpp

#include <iostream>
#include <vector>
#include <typeinfo>
#include <variant>
#include <string>

template<typename ... Ts> // (7) 
struct Overload : Ts ... { 
 using Ts::operator() ...;
};
template<class... Ts> Overload(Ts...) -> Overload<Ts...>;

int main(){
 
 std::cout << '\n';
 
 std::vector<std::variant<char, long, float, int, double, long long>> // (1) 
 vecVariant = {5, '2', 5.4, 100ll, 2011l, 3.5f, 2017};

 auto TypeOfIntegral = Overload { // (2)
 [](char) { return "char"; },
 [](int) { return "int"; },
 [](unsigned int) { return "unsigned int"; },
 [](long int) { return "long int"; },
 [](long long int) { return "long long int"; },
 [](auto) { return "unknown type"; },
 };
 
 for (auto v : vecVariant) { // (3)
 std::cout << std::visit(TypeOfIntegral, v) << '\n';
 }

 std::cout << '\n';

 std::vector<std::variant<std::vector<int>, double, std::string>> // (4)
 vecVariant2 = { 1.5, std::vector<int>{1, 2, 3, 4, 5}, "Hello "};

 auto DisplayMe = Overload { // (5)
 [](std::vector<int>& myVec) { 
 for (auto v: myVec) std::cout << v << " ";
 std::cout << '\n'; 
 },
 [](auto& arg) { std::cout << arg << '\n';},
 };

 for (auto v : vecVariant2) { // (6)
 std::visit(DisplayMe, v);
 }

 std::cout << '\n';
 
}

行 (1) は整数型を持つバリアントのベクトルを作成し、行 (4) は 639 を持つバリアントのベクトルを作成します。 ,646 、および 654 .

最初の変種 669 を続けましょう . TypeOfIntegral (2) は、いくつかの整数型に対して文字列表現を返すオーバーロード セットです。型がオーバーロード セットによって処理されない場合、文字列 "673 を返します。 ". 行 (3) では、各バリアントにオーバーロード セットを適用します683 698 を使用 .

2 番目のバリアント vecVariant2 (4) には、複合型があります。それらの値を表示するオーバーロード セット (5) を作成します。一般に、値を 705 にプッシュするだけです。 . 718 の場合 、範囲ベースの for ループを使用して値を 725 にプッシュします .

最後に、これがプログラムの出力です。

この例 (7) で使用されているオーバーロード パターンにいくつかの単語を追加したいと思います。前回の投稿「パラメーター パックとフォールド式を使用したスマート トリック`」で既に紹介しました。

template<typename ... Ts> // (1)
struct Overload : Ts ... { 
 using Ts::operator() ...;
};
template<class... Ts> Overload(Ts...) -> Overload<Ts...>; // (2)

行 (1) は過負荷パターンで、行 (2) はその控除ガイドです。構造体 736 任意の数の基本クラスを持つことができます (741 )。各クラス 756 から派生します 呼び出し演算子 (768 .) 各基本クラスをそのスコープに。基本クラスには、オーバーロードされた呼び出し演算子 (Ts::operator()) が必要です。ラムダは、この呼び出し演算子を提供します。次の例は、可能な限り単純です。

#include <variant>

template<typename ... Ts> 
struct Overload : Ts ... { 
 using Ts::operator() ...;
};
template<class... Ts> Overload(Ts...) -> Overload<Ts...>;

int main(){
 
 std::variant<char, int, float> var = 2017;

 auto TypeOfIntegral = Overload {  // (1)  
 [](char) { return "char"; },
 [](int) { return "int"; },
 [](auto) { return "unknown type"; },
 };
 
}

この例を C++ Insights で使用すると、魔法が透過的になります。まず、(1) を呼び出すと、完全に特殊化されたクラス テンプレートが作成されます。

次に、777 などのオーバーロード パターンで使用されるラムダ 関数オブジェクトを作成します。この場合、コンパイラは関数オブジェクトに 781 という名前を付けます。 .

自動生成タイプを調べると、少なくとも 1 つの興味深い点が示されます。 __lambda_15_9 の呼び出し演算子は 790 に対してオーバーロードされています

控除ガイド (804 ) (2 行目) は C++17 でのみ必要です。演繹ガイドは、コンストラクター外の引数テンプレート パラメーターを作成する方法をコンパイラーに指示します。 C++20 では、テンプレートを自動的に推測できます。

次は?

テンプレートの友情は特別です。次回の投稿では、その理由を説明します。