C++17完整导引-组件之std::variant

引子

类模板std::variant表示一个类型安全的联合体(以下称“变化体”)。std::visit可以处理std::variant中的值。它最大的优势是提供了一种新的具有多态性的处理异质集合的方法。也就是说,它可以帮助我们处理不同类型的数据,并且不需要公共基类和指针

动机

起源于C语言,C++也提供对union的支持,它的作用是持有一个值,这个值的类型可能是指定的若干类型中的任意 一个 。然而,这项语言特性有一些缺陷

  • 对象并不知道它们现在持有的值的类型。因此,你不能持有非平凡类型,例如std::string(没有进行特殊处理的话)。
  • 不能从union派生。
  • 它们不尊重对象的生命周期,也就是说,当您更改所包含的类型时,不会调用构造函数或析构函数
  • 访问错误的元素是未定义的行为。

示例

#include <iostream>
#include <string>
#include <vector>

union S {
    std::string str;
    std::vector<int> vec;
    ~S() { } // what to delete here?
};
int main()
{
    S s = {"Hello, world"};
    // 此时,从s.vec读取是未定义的行为
    std::cout << "s.str = " << s.str << '\n';
    // 你必须调用所包含对象的析构函数!
    s.str.~basic_string<char>();
    // 还有一个构造函数!
    new (&s.vec) std::vector<int>;
    // 现在,s.vec是union的活跃成员 
    s.vec.push_back(10);
    std::cout << s.vec.size() << '\n';
    // 另一个析构函数
    s.vec.~vector<int>();
}

通过std::variant<>C++标准库提供了一种 可辨识的联合(closed discriminated union) (这意味着要指明一个可能的类型列表

  • 当前值的类型已知
  • 可以持有任何类型的值
  • 可以从它派生

事实上,一个std::variant<>持有的值有若干 候选项(alternative) ,这些选项通常有不同的类型。然而,两个不同选项的类型也有可能相同,这在多个类型相同的选项分别代表不同含义的时候很有用(例如,可能有两个选项类型都是字符串,分别代表数据库中不同列的名称,你可以知道当前的值代表哪一个列)。
variant所占的内存大小等于所有可能的底层类型中最大的再加上一个记录当前选项的固定内存开销。不会分配堆内存。
一般情况下,除非你指定了一个候选项来表示为空,否则variant不可能为空。然而,在非常罕见的情况下(例如赋予一个不同类型新值时发生了异常),variant可能会变为没有值的状态。
std::optional<>std::any一样,variant对象是值语义。也就是说,拷贝被实现为 深拷贝 ,将会创建一个在自己独立的内存空间内存储有当前选项的值的新对象。然而,拷贝std::variant<>的开销要比拷贝当前选项的开销稍微大一点,这是因为variant必须找出要拷贝哪个值。另外,variant也支持move语义。
示例:

#include <iostream>
#include <string>
#include <variant>

using namespace std;

struct SampleVisitor {
    void operator()(int i) const { cout << "int: " << i << "\n"; }
    void operator()(float f) const { cout << "float: " << f << "\n"; }
    void operator()(const string& s) const { cout << "string: " << s << "\n"; }
};

int main() {
    variant<int, float, string> intFloatString;
    static_assert(variant_size_v<decltype(intFloatString)> == 3);

    // 默认初始化为第一个选项,应该是0
    visit(SampleVisitor{}, intFloatString);

    // 索引将显示当前使用的 'type'
    cout << "index = " << intFloatString.index() << endl;
    intFloatString = 100.0f;
    cout << "index = " << intFloatString.index() << endl;
    intFloatString = "hello super world";
    cout << "index = " << intFloatString.index() << endl;

    // try with get_if:
    if (const auto intPtr(get_if<int>(&intFloatString)); intPtr)
        cout << "int!" << *intPtr << "\n";
    else if (const auto floatPtr(get_if<float>(&intFloatString)); floatPtr)
        cout << "float!" << *floatPtr << "\n";

    if (holds_alternative<int>(intFloatString))
        cout << "the variant holds an int!\n";
    else if (holds_alternative<float>(intFloatString))
        cout << "the variant holds a float\n";
    else if (holds_alternative<string>(intFloatString))
        cout << "the variant holds a string\n";

    // try/catch and bad_variant_access
    try {
        auto f = get<float>(intFloatString);
        cout << "float! " << f << "\n";
    } catch (bad_variant_access&) {
        cout << "our variant doesn't hold float at this moment...\n";
    }

    // visit:
    visit(SampleVisitor{}, intFloatString);
    intFloatString = 10;
    visit(SampleVisitor{}, intFloatString);
    intFloatString = 10.0f;
    visit(SampleVisitor{}, intFloatString);
}

运行结果如下:

int: 0
index = 0
index = 1
index = 2
the variant holds a string
our variant doesn't hold float at this moment...
string: hello super world
int: 10
float: 10

上面的例子中展示了几件事:

  • 可以通过index()holds_alternative来了解当前使用的类型。
  • 可以使用get_ifget来访问该值(但这可能会抛出bad_variant_access异常)。
  • 类型安全:变体不允许获取非活动类型的值
  • 如果不使用值初始化变量,则使用第一个类型初始化该变量。在这种情况下,第一个可选类型必须具有默认构造函数。
  • 不会发生额外的堆分配
  • 可以使用访问器来调用当前保持类型上的某些操作。。
  • variant 类调用非平凡类型的析构函数和构造函数,因此在本例中,字符串对象在切换到新的变体之前被清理。

使用

下面的代码展示了std::variant<>的核心功能:

#include <variant>
#include <iostream>

int main()
{
    std::variant<int, std::string> var{"hi"};   // 初始化为string选项
    std::cout << var.index() << '\n';           // 打印出1
    var = 42;                                   // 现在持有int选项
    std::cout << var.index() << '\n';           // 打印出0
    ...
    try {
        int i = std::get<0>(var);                   // 通过索引访问
        std::string s = std::get<std::string>(var); // 通过类型访问(这里会抛出异常)
        ...
    }
    catch (const std::bad_variant_access& e) {      // 当索引/类型错误时进行处理
        std::cerr << "EXCEPTION: " << e.what() << '\n';
        ...
    }
}

成员函数index()可以用来指出当前选项的索引(第一个选项的索引是0)。

初始化和赋值操作都会查找最匹配的选项。如果类型不能精确匹配,可能会发生奇怪的事情。
注意variant、有引用成员的variant、有C风格数组成员的variant、有不完全类型(例如void)的variant都是不允许的。(variant不能保有引用、数组,或类型 void。空variant也非良构(可用 std::variantstd::monostate 代替) )
variant没有空的状态。这意味着每一个构造好的variant对象,至少调用了一次构造函数。默认构造函数会调用第一个选项类型的默认构造函数

std::variant<std::string, int> var;     // => var.index()==0, 值==""

如果第一个类型没有默认构造函数,那么调用variant的默认构造函数将会导致编译期错误:

struct NoDefConstr {
    NoDefConstr(int i) {
        std::cout << "NoDefConstr::NoDefConstr(int) called\n";
    }
};
std::variant<NoDefConstr, int> v1;      // ERROR:不能默认构造第一个选项

辅助类型std::monostate提供了处理这种情况的能力,还可以用来模拟空值的状态。

std::monostate 占位符类型

有意为行为良好的std::variant中空可选项所用的单位类型。具体而言,非可默认构造的variant可以列std::monostate为其首个可选项:这使得variant自身可默认构造。

为了支持第一个类型没有默认构造函数的variantC++标准库提供了一个特殊的辅助类:std::monostate
std::monostate类型的对象总是处于相同的状态。因此,比较它们的结果总是相等。它的作用是可以作为一个选项,当variant处于这个选项时表示此variant 没有其他任何类型的值

因此,std::monostate可以作为第一个选项类型来保证variant能默认构造。例如:

std::variant<std::monostate, NoDefConstr, int> v2;  // OK
std::cout << "index: " << v2.index() << '\n';       // 打印出0

某种意义上,你可以把这种状态解释为variant为空的信号。
下面的代码展示了几种检测monostate的方法,也同时展示了variant的其他一些操作:

if (v2.index() == 0) {
    std::cout << "has monostate\n";
}
if (!v2.index()) {
    std::cout << "has monostate\n";
}
if (std::holds_alternative<std::monostate>(v2)) {
    std::cout << "has monostate\n";
}
if (std::get_if<0>(&v2)) {
    std::cout << "has monostate\n";
}
if (std::get_if<std::monostate>(&v2)) {
    std::cout << "has monostate\n";
}

get_if<>()的参数是一个指针,并在当前选项为T时返回一个指向当前选项的指针,否则返回nullptr。这和get<T>()不同,后者获取variant的引用作为参数并在提供的索引或类型正确时以值返回当前选项,否则抛出异常。和往常一样,你可以赋予variant一个和当前选项类型不同的其他选项的值,甚至可以赋值为monostate来表示为空:

v2 = 42;
std::cout << "index: " << v2.index() << '\n';   // index:2

v2 = std::monostate{};
std::cout << "index: " << v2.index() << '\n';   // index: 0

完整示例如下:

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

using namespace std;

struct NoDefConstr {
    NoDefConstr(int i) { cout << "NoDefConstr::NoDefConstr(int) called\n"; }
};

int main() {
    variant<monostate, NoDefConstr, int> v2;  // OK
    cout << "index: " << v2.index() << '\n';  // 打印出0

    if (v2.index() == 0) {
        cout << "has monostate\n";
    }
    if (!v2.index()) {
        cout << "has monostate\n";
    }
    if (holds_alternative<monostate>(v2)) {
        cout << "has monostate\n";
    }
    if (get_if<0>(&v2)) {
        cout << "has monostate\n";
    }
    if (get_if<monostate>(&v2)) {
        cout << "has monostate\n";
    }

    v2 = 42;
    cout << "index: " << v2.index() << '\n';  // index:2

    v2 = monostate{};
    cout << "index: " << v2.index() << '\n';  // index: 0
}

运行结果如下:

index: 0
has monostate
has monostate
has monostate
has monostate
has monostate
index: 2
index: 0

从variant派生

你可以从variant派生。例如,你可以定义如下派生自std::variant<>的聚合体:

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

using namespace std;

class Derived : public std::variant<int, std::string> {};

int main() {
    Derived d = {{"hello"}};
    std::cout << d.index() << '\n';       // 打印出:1
    std::cout << std::get<1>(d) << '\n';  // 打印出:hello
    d.emplace<0>(77);                     // 初始化为int,销毁string
    std::cout << std::get<0>(d) << '\n';  // 打印出:77
}

运行结果如下:

1
hello
77

预处理代码如下:

class Derived : public std::variant<int, std::basic_string<char> >
{
  public: 
  // inline ~Derived() noexcept = default;
};
int main()
{
  Derived d = {std::variant<int, std::basic_string<char> >{"hello"}};
  std::operator<<(std::cout.operator<<(static_cast<const std::variant<int, std::basic_string<char> >&>(d).index()), '\n');
  std::operator<<(std::operator<<(std::cout, std::get<1>(static_cast<std::variant<int, std::basic_string<char> >&>(d))), '\n');
  static_cast<std::variant<int, std::basic_string<char> >&>(d).emplace<0>(77);
  std::operator<<(std::cout.operator<<(std::get<0>(static_cast<std::variant<int, std::basic_string<char> >&>(d))), '\n');
  return 0;
}

类型和操作

类型

在头文件variantC++标准库以如下方式定义了类std::variant<>

namespace std {
    template<typename... Types> class variant;
    // 译者注:此处原文的定义是
    // template<typename Types...> class variant;
    // 应是作者笔误
}

也就是说,std::variant<>是一个 可变参数(variadic) 类模板(C++11引入的处理任意数量参数的特性)。
另外,还定义了下面的类型和对象:

  • 类模板std::variant_size
  • 类模板std::variant_alternative
  • std::variant_npos
  • 类型std::monostate
  • 异常类std::bad_variant_access,派生自std::exception

还有两个为variant定义的变量模板:std::in_place_type<>std::in_place_index<>。它们的类型分别是std::in_place_type_tstd::in_place_index_t,定义在头文件<utility>中。

操作

下表列出了std::variant<>的所有操作。

操作符效果
构造函数创建一个variant对象(可能会调用底层类型的构造函数)
析构函数销毁一个variant对象
=赋新值
emplace<T>()销毁旧值并赋一个T类型选项的新值
emplace<Idx>()销毁旧值并赋一个索引为Idx的选项的新值
valueless_by_exception()返回变量是否因为异常而没有值
index()返回当前选项的索引
swap()交换两个对象的值
==、!=、<、<=、>、>=比较variant对象
hash<>计算哈希值的函数对象类型
holds_alternative<T>()返回是否持有类型T的值
get<T>()返回类型为T的选项的值
get<Idx>()返回索引为Idx的选项的值
get_if<T>()返回指向类型为T的选项的指针或nullptr
get_if<Idx>()返回指向索引为Idx的选项的指针或nullptr
visit()对当前选项进行操作

构造函数

  1. 默认构造函数constexpr variant() noexcept(/* see below */);

构造 variant ,保有首个可选项的值初始化的值(index()为零)。

  • 当且仅当可选项类型T_0的值初始化满足 constexpr 函数的要求,此构造函数才为constexpr
  • 此重载只有在std::is_default_constructible_v<T_0> true 时才会参与重载决议。
std::variant<int, std::string> var; // 值初始化第一个可选项,第一个int初始化为0,index()==0
cout<< "index " << var.index() << " ; holds_alternative = "<<holds_alternative<int>(var) <<" ; get<int>(var) = " << std::get<int>(var)<<endl;

选项被默认初始化,意味着基本类型会初始化为0nullptr。运行结果:

index 0 ; holds_alternative = 1 ; get<int>(var) = 0
  1. 复制构造函数constexpr variant( const variant& other );

other非因异常无值,则构造一个保有与other相同可选项的 variant ,并以std::get<other.index()>(other) 直接初始化所含值。否则,初始化一个因异常无值的 variant

  • 此构造函数定义为被删除,除非std::is_copy_constructible_v<T_i>对于所有Types...中的T_i true
  • std::is_trivially_copy_constructible_v<T_i>Types...中的所有 T_i true 则它为平凡。
variant<string, int> var{"STR"};// 用 string{"STR"}; 初始化第一个可选项
cout << "var " << get<string>(var) << endl;
auto var1 (var); // 用 var初始化
cout << "var1 " << get<string>(var1) << "  "<< var1.index() << endl;
  1. 移动构造函数constexpr variant( variant&& other ) noexcept(/* see below */);

other 非因异常无值,则构造一个保有与 other 相同可选项的variant并以 std::get<other.index()>(std::move(other)) 直接初始化所含值。否则,初始化一个因异常无值的 variant

  • 此重载只有在 std::is_move_constructible_v<T_i> 对于所有Types... 中的T_itrue 时才会参与重载决议。
  • std::is_trivially_move_constructible_v<T_i>Types... 中的所有T_itrue 则它为平凡。
  1. 转换构造函数
template< class T >
constexpr variant( T&& t ) noexcept(/* see below */);

构造保有会被重载决议对表达式 F(std::forward<T>(t)) 选择的可选项T_j,假设对来自Types...中的每个T_i同时存在一个虚构函数F(T_i)的重载,除了:

  • 仅若声明 T_i x[] = { std::forward<T>(t) }; 对某个虚设变量 x 才考虑 F(T_i)

如同用直接非列表初始化从std::forward<T>(t)直接初始化所含值。

  • 此重载只有在
    • sizeof...(Types) > 0
    • std::decay_t<U> (C++20 前)std::remove_cvref_t<U> (C++20 起) 既不与variant为同一类型,亦非std::in_place_type_tstd::in_place_index_t 的特化,
    • std::is_constructible_v<T_j, T> 为 true ,
    • 且表达式F(std::forward<T>(t))(令 F 为上述虚构函数的重载集)为良构时才会参与重载决议。
      T_j 的被选择构造函数为 constexpr 构造函数,则此构造函数为constexpr构造函数。
std::variant<std::string> v("abc"); // OK
std::variant<std::string, std::string> w("abc"); // 谬构
std::variant<std::string, const char*> x("abc"); // OK :选择 const char*
std::variant<std::string, bool> y("abc"); // OK :选择 string ; bool 不是候选
std::variant<float, long, double> z = 0; // OK :保有 long
                                         // float 与 double 不是候选
  1. 构造一个有指定可选项类型Tvariant 并以参数std::forward<Args>(args)...初始化所含值。
template< class T, class... Args >
constexpr explicit variant( std::in_place_type_t<T>, Args&&... args );
  • T 的被选择构造函数是 constexpr 构造函数,则此构造函数亦为 constexpr 构造函数。
  • 此重载只有在 Types... 中正好出现一次 Tstd::is_constructible_v<T, Args...>true 时才会参与重载决议。
  1. 构造一个有指定可选项类型 Tvariant 并以参数il, std::forward<Args>(args)...初始化所含值。
template< class T, class U, class... Args >
constexpr explicit variant( std::in_place_type_t<T>,
                            std::initializer_list<U> il, Args&&... args );
  • 若 T 的被选择构造函数是 constexpr 构造函数,则此构造函数亦为constexpr构造函数。
  • 此重载只有在 Types... 中正好出现一次 T std::is_constructible_v<T, initializer_list<U>&, Args...>true时才会参与重载决议。
  1. 构造一个有下标 I 所指定的可选项类型 T_i variant 并以参数std::forward<Args>(args)... 初始化所含值。
template< std::size_t I, class... Args >
constexpr explicit variant( std::in_place_index_t<I>, Args&&... args );
  • T_i 的被选择构造函数是 constexpr 构造函数,则此构造函数亦为 constexpr 构造函数。
  • 此重载只有在I < sizeof...(Types)std::is_constructible_v<T_i, Args...>皆为 true 时才会参与重载决议。
  1. 构造一个有下标I所指定的可选项类型T_ivariant 并以参数il, std::forward<Args>(args)... 初始化所含值。
template< std::size_t I, class U, class... Args >
constexpr explicit variant( std::in_place_index_t<I>,
                            std::initializer_list<U> il, Args&&... args );
  • T_i 的被选择构造函数是constexpr构造函数,则此构造函数亦为constexpr构造函数。
  • 此重载只有在 I < sizeof...(Types)std::is_constructible_v<T_i, std::initializer_list<U>&, Args...> 皆为true时才会参与重载决议。

示例1 如果传递一个值来初始化,将会使用最佳匹配的类型:

std::variant<long, int> v2{42};
std::cout << v2.index() << '\n';            // 打印出1

示例2如果有两个类型同等匹配会导致歧义:

std::variant<long, long> v3{42};            // ERROR:歧义
std::variant<int, float> v4{42.3};          // ERROR:歧义
std::variant<int, double> v5{42.3};         // OK
std::variant<int, long double> v6{42.3};    // ERROR:歧义

std::variant<std::string, std::string_view> v7{"hello"};                // ERROR:歧义
std::variant<std::string, std::string_view, const char*> v8{"hello"};   // OK
std::cout << v8.index() << '\n';                                        // 打印出2

示例3 为了传递多个值来调用构造函数初始化,你必须使用in_place_type或者in_place_index标记:

std::variant<std::complex<double>> v9{3.0, 4.0};    // ERROR
std::variant<std::complex<double>> v10{{3.0, 4.0}}; // ERROR
std::variant<std::complex<double>> v11{std::in_place_type<std::complex<double>>, 3.0, 4.0};
std::variant<std::complex<double>> v12{std::in_place_index<0>, 3.0, 4.0};

你也可以使用in_place_index在初始化时解决歧义问题或者打破匹配优先级:

std::variant<int, int> v13{std::in_place_index<1>, 77};     // 初始化第二个int
std::variant<int, long> v14{std::in_place_index<1>, 77};    // 初始化long,而不是int
std::cout << v14.index() << '\n';       // 打印出1

示例4 传递一个带有其他参数的初值列:

// 用一个lambda作为排序准则初始化一个set的variant:
auto sc = [] (int x, int y) {
              return std::abs(x) < std::abs(y);
          };
std::variant<std::vector<int>, std::set<int, decltype(sc)>>
    v15{std::in_place_index<1>, {4, 8, -7, -2, 0, 5}, sc};

示例5 只有当所有初始值都和容器里元素类型匹配时才可以这么做。否则你必须显式传递一个std::initializer_list<>

// 用一个lambda作为排序准则初始化一个set的variant
auto sc = [] (int x, int y) {
              return std::abs(x) < std::abs(y);
          };
std::variant<std::vector<int>, std::set<int, decltype(sc)>>
    v15{std::in_place_index<1>, std::initializer_list<int>{4, 5L}, sc};

示例6 std::variant<>不支持类模板参数推导,也没有make_variant<>()快捷函数(不像std::optional<>std::any)。这样做也没有意义,因为variant目标是处理多个候选项
如果所有的候选项都支持拷贝,那么就可以拷贝variant对象:

struct NoCopy {
    NoCopy() = default;
    NoCopy(const NoCopy&) = delete;
};

std::variant<int, NoCopy> v1;
std::variant<int, NoCopy> v2{v1};   // ERROR

综合示例

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

int main() {
    // 默认构造
    std::variant<int, float> intFloat;
    std::cout << intFloat.index() << ", value " << std::get<int>(intFloat)
              << "\n";

    // monostate for default initialization:
    class NotSimple {
       public:
        NotSimple(int, float) {}
    };

    // std::variant<NotSimple, int> cannotInit; // error
    std::variant<std::monostate, NotSimple, int> okInit;
    std::cout << okInit.index() << "\n";

    // 传值:
    std::variant<int, float, std::string> intFloatString{10.5f};
    std::cout << intFloatString.index() << ", value "
              << std::get<float>(intFloatString) << "\n";

    // 歧义
    // double 可能会转换为浮点型或 int,因此编译器无法决定
    // std::variant<int, float, std::string> intFloatString { 10.5 };

    // 使用in_place起义
    std::variant<long, float, std::string> longFloatString{
        std::in_place_index<1>, 7.6};  // double!
    std::cout << longFloatString.index() << ", value "
              << std::get<float>(longFloatString) << "\n";

    // in_place 复杂的类型
    std::variant<std::vector<int>, std::string> vecStr{std::in_place_index<0>,
                                                       {0, 1, 2, 3}};
    std::cout << vecStr.index() << ", vector size "
              << std::get<std::vector<int>>(vecStr).size() << "\n";

    // 从其他variant复制初始化:
    std::variant<int, float> intFloatSecond{intFloat};
    std::cout << intFloatSecond.index() << ", value "
              << std::get<int>(intFloatSecond) << "\n";
}

运行结果如下:

ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 0
0, value 0
0
1, value 10.5
1, value 7.6
0, vector size 4
0, value 0

访问值

通常的方法是调用get<>()get_if<>访问当前选项的值。你可以传递一个索引、或者传递一个类型(该类型的选项只能有一个)。使用一个无效的索引和无效/歧义的类型将会导致编译错误。如果访问的索引或者类型不是当前的选项,将会抛出一个std::bad_variant_access异常。
归结如下:

  • 以不匹配当前活跃可选项的下标或类型调用 std::get(std::variant)
  • 调用 std::visit 观览因异常无值 (valueless_by_exception) variant
  1. get() 函数
template< std::size_t I, class... Types >
constexpr std::variant_alternative_t<I, std::variant<Types...>>&
    get( std::variant<Types...>& v );
  • 基于下标的值访问器:若v.index() == I,则返回到存储于v的值的引用。否则抛出 std::bad_variant_access 。若I不是 variant 的合法下标,则此调用为病式。
  • 基于类型的值访问器:若 v 保有可选项 T ,则返回到存储于v的值的引用。否则抛出 std::bad_variant_access 。若 T 不是 Types... 中唯一存在的元素,则此调用为病式。

例1:

std::variant<int, int, std::string> var;    // 第一个int设为0,index()==0

auto a = std::get<double>(var);             // 编译期ERROR:没有double类型的选项
auto b = std::get<4>(var);                  // 编译期ERROR:没有第五个选项
auto c = std::get<int>(var);                // 编译期ERROR:有两个int类型的选项

try {
    auto s = std::get<std::string>(var);    // 抛出异常(当前选项是第一个int)
    auto i = std::get<0>(var);              // OK,i==0
    auto j = std::get<1>(var);              // 抛出异常(当前选项是另一个int)
}
catch (const std::bad_variant_access& e) {
    std::cout << "Exception: " << e.what() << '\n';
}

例2:

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

using namespace std;

int main() {
    std::variant<int, float> v{12}, w;
    int i = std::get<int>(v);
    w = std::get<int>(v);
    cout << "w = " << std::get<int>(w) << endl;
    w = std::get<0>(v);  // 效果同前一行
    cout << "w = " << std::get<int>(w) << endl;
    //  std::get<double>(v); // 错误: [int, float] 中无 double
    //  std::get<3>(v);      // 错误:合法的 index 值是 0 和 1

    try {
        std::get<float>(w);  // w 含有 int ,非 float :将抛出异常
    } catch (std::bad_variant_access& e) {
        cout << "Exception:" << e.what() << endl;
    }
}

运行结果如下:

w = 12
w = 12
Exception:std::get: wrong index for variant

预处理代码如下:

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

using namespace std;

int main()
{
  std::variant<int, float> v = std::variant<int, float>{12};
  std::variant<int, float> w = std::variant<int, float>();
  int i = std::get<int>(v);
  w.operator=(std::get<int>(v));
  std::operator<<(std::cout, "w = ").operator<<(std::get<int>(w)).operator<<(std::endl);
  w.operator=(std::get<0>(v));
  std::operator<<(std::cout, "w = ").operator<<(std::get<int>(w)).operator<<(std::endl);
  try 
  {
    std::get<float>(w);
  } catch(std::bad_variant_access & e) {
    std::operator<<(std::operator<<(std::cout, "Exception:"), e.what()).operator<<(std::endl);
  }
  ;
  return 0;
}
  1. get_if() 返回指向存储于被指向的 variant 中值的指针,错误时为空指针。
template< std::size_t I, class... Types >
constexpr std::add_pointer_t<std::variant_alternative_t<I, std::variant<Types...>>>
    get_if( std::variant<Types...>* pv ) noexcept;
  • 基于下标的不抛出访问器:若 pv 不是空指针且pv->index() == I,则返回指向存储于 pv 所指向的 variant 中的值的指针。否则,返回空指针值。若 I 不是variant的合法下标,则此调用良构。
  • 基于类型的不抛出访问器I TTypes...中的零基下标。若 T 不是 Types... 中的唯一存在的元素,则此调用非良构。

get_if()可以在访问值之前先检查给定的选项是否是当前选项

if (auto ip = std::get_if<1>(&var); ip != nullptr) {
    std::cout << *ip << '\n';
}
else {
    std::cout << "alternative with index 1 not set\n";
}

这里还使用了带初始化的if语句,把初始化过程和条件检查分成了两条语句。你也可以直接把初始化语句用作条件语句:

if (auto ip = std::get_if<1>(&var)) {
    std::cout << *ip << '\n';
}
else {
    std::cout << "alternative with index 1 not set\n";
}

示例:

#include <iostream>
#include <variant>

int main() {
    std::variant<int, float> v{12};

    if (auto pval = std::get_if<float>(&v))
        std::cout << "variant value: " << *pval << '\n';
    else
        std::cout << "failed to get value!" << '\n';
}

运行结果

failed to get value!

预处理代码

#include <iostream>
#include <variant>

int main()
{
  std::variant<int, float> v = std::variant<int, float>{12};
  {
    float * pval = std::get_if<float>(&v);
    if(pval) {
      std::operator<<(std::operator<<(std::cout, "variant value: ").operator<<(*pval), '\n');
    } else {
      std::operator<<(std::operator<<(std::cout, "failed to get value!"), '\n');
    } 
    
  }
  
  return 0;
}

另一种访问不同选项的值的方法是使用variant访问器。

修改值

赋值操作和emplace()函数可以修改值

std::variant<int, int, std::string> var; // 设置第一个int为0,index()==0
var = "hello";         // 设置string选项,index()==2
var.emplace<1>(42);     // 设置第二个int,index()==1

预处理代码如下:

  std::variant<int, int, std::basic_string<char> > var = std::variant<int, int, std::basic_string<char> >();
  var.operator=("hello");
  var.emplace<1>(42);

注意 operator=将会直接赋予variant一个新值只要有和新值类型对应的选项emplace()赋予新值之前会先销毁旧的值
你也可以使用get<>()或者get_if<>()来给当前选项赋予新值

std::variant<int, int, std::string> var; // 设置第一个int为0,index()==0
std::get<0>(var) = 77;                   // OK,但当前选项仍是第一个int
std::get<1>(var) = 99;                   // 抛出异常(因为当前选项是另一个int)

if (auto p = std::get_if<1>(&var); p) {  // 如果第二个int被设置
    *p = 42;                             // 修改值
}

std::get<1>(var) = 99; 抛出如下异常:

terminate called after throwing an instance of 'std::bad_variant_access'
  what():  std::get: wrong index for variant

预处理代码如下:

  std::variant<int, int, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > var = std::variant<int, int, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >();
  std::get<0>(var) = 77;
  {
    int * p = std::get_if<1>(&var);
    if(p) {
      *p = 42;
    } 
    
  }

另一个修改不同选项的值的方法是variant访问器。

比较

对两个类型相同的variant(也就是说,它们每个选项的类型和顺序都相同),你可以使用通常的比较运算符。

比较运算将遵循如下规则:

  • 当前选项索引较小的小于当前选项索引较大的。
  • 如果两个variant当前的选项相同,将调用当前选项类型的比较运算符进行比较。
    注意所有的std::monostate类型的对象都相等。
  • 两个variant都处于特殊状态(valueless_by_exception()为真的状态)时相等。否则,valueless_by_exception()返回turevariant小于另一个。

例如:

#include <iostream>
#include <string>
#include <variant>

int main() {
    std::cout << std::boolalpha;
    std::string cmp;
    bool result;

    auto print2 = [&cmp, &result](const auto& lhs, const auto& rhs) {
        std::cout << lhs << ' ' << cmp << ' ' << rhs << " : " << result << '\n';
    };

    std::variant<int, std::string> v1, v2;

    std::cout << "operator==\n";
    {
        cmp = "==";

        // by default v1 = 0, v2 = 0;
        result = v1 == v2;  // true
        std::visit(print2, v1, v2);

        v1 = v2 = 1;
        result = v1 == v2;  // true
        std::visit(print2, v1, v2);

        v2 = 2;
        result = v1 == v2;  // false
        std::visit(print2, v1, v2);

        v1 = "A";
        result = v1 == v2;  // false: v1.index == 1, v2.index == 0
        std::visit(print2, v1, v2);

        v2 = "B";
        result = v1 == v2;  // false
        std::visit(print2, v1, v2);

        v2 = "A";
        result = v1 == v2;  // true
        std::visit(print2, v1, v2);
    }

    std::cout << "operator<\n";
    {
        cmp = "<";

        v1 = v2 = 1;
        result = v1 < v2;  // false
        std::visit(print2, v1, v2);

        v2 = 2;
        result = v1 < v2;  // true
        std::visit(print2, v1, v2);

        v1 = 3;
        result = v1 < v2;  // false
        std::visit(print2, v1, v2);

        v1 = "A";
        v2 = 1;
        result = v1 < v2;  // false: v1.index == 1, v2.index == 0
        std::visit(print2, v1, v2);

        v1 = 1;
        v2 = "A";
        result = v1 < v2;  // true: v1.index == 0, v2.index == 1
        std::visit(print2, v1, v2);

        v1 = v2 = "A";
        result = v1 < v2;  // false
        std::visit(print2, v1, v2);

        v2 = "B";
        result = v1 < v2;  // true
        std::visit(print2, v1, v2);

        v1 = "C";
        result = v1 < v2;  // false
        std::visit(print2, v1, v2);
    }

    {
        std::variant<int, std::string> v1;
        std::variant<std::string, int> v2;
        //  v1 == v2;  // Compilation error: no known conversion
    }
}

运行结果如下:

operator==
0 == 0 : true
1 == 1 : true
1 == 2 : false
A == 2 : false
A == B : false
A == A : true
operator<
1 < 1 : false
1 < 2 : true
3 < 2 : false
A < 1 : false
1 < A : true
A < A : false
A < B : true
C < B : false

预处理代码如下:

#include <iostream>
#include <string>
#include <variant>

int main()
{
  std::cout.operator<<(std::boolalpha);
  std::basic_string<char> cmp = std::basic_string<char>();
  bool result;
    
  class __lambda_10_19
  {
    public: 
    template<class type_parameter_0_0, class type_parameter_0_1>
    inline /*constexpr */ auto operator()(const type_parameter_0_0 & lhs, const type_parameter_0_1 & rhs) const
    {
      (((((((std::cout << lhs) << ' ') << cmp) << ' ') << rhs) << " : ") << result) << '\n';
    }
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ void operator()<int, int>(const int & lhs, const int & rhs) const
    {
      std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(lhs), ' '), cmp), ' ').operator<<(rhs), " : ").operator<<(result), '\n');
    }
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ void operator()<std::basic_string<char>, std::basic_string<char> >(const std::basic_string<char> & lhs, const std::basic_string<char> & rhs) const
    {
      std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, lhs), ' '), cmp), ' '), rhs), " : ").operator<<(result), '\n');
    }
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ void operator()<std::basic_string<char>, int>(const std::basic_string<char> & lhs, const int & rhs) const
    {
      std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, lhs), ' '), cmp), ' ').operator<<(rhs), " : ").operator<<(result), '\n');
    }
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ void operator()<int, std::basic_string<char> >(const int & lhs, const std::basic_string<char> & rhs) const
    {
      std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(lhs), ' '), cmp), ' '), rhs), " : ").operator<<(result), '\n');
    }
    #endif
    
    private: 
    std::basic_string<char> & cmp;
    bool & result;
    
    public:
    __lambda_10_19(std::basic_string<char> & _cmp, bool & _result)
    : cmp{_cmp}
    , result{_result}
    {}
    
  };
  
  __lambda_10_19 print2 = __lambda_10_19{cmp, result};
  std::variant<int, std::basic_string<char> > v1 = std::variant<int, std::basic_string<char> >();
  std::variant<int, std::basic_string<char> > v2 = std::variant<int, std::basic_string<char> >();
  std::operator<<(std::cout, "operator==\n");
  {
    cmp.operator=("==");
    result = std::operator==(v1, v2);
    std::visit(print2, v1, v2);
    v1.operator=(v2.operator=(1));
    result = std::operator==(v1, v2);
    std::visit(print2, v1, v2);
    v2.operator=(2);
    result = std::operator==(v1, v2);
    std::visit(print2, v1, v2);
    v1.operator=("A");
    result = std::operator==(v1, v2);
    std::visit(print2, v1, v2);
    v2.operator=("B");
    result = std::operator==(v1, v2);
    std::visit(print2, v1, v2);
    v2.operator=("A");
    result = std::operator==(v1, v2);
    std::visit(print2, v1, v2);
  };
  std::operator<<(std::cout, "operator<\n");
  {
    cmp.operator=("<");
    v1.operator=(v2.operator=(1));
    result = std::operator<(v1, v2);
    std::visit(print2, v1, v2);
    v2.operator=(2);
    result = std::operator<(v1, v2);
    std::visit(print2, v1, v2);
    v1.operator=(3);
    result = std::operator<(v1, v2);
    std::visit(print2, v1, v2);
    v1.operator=("A");
    v2.operator=(1);
    result = std::operator<(v1, v2);
    std::visit(print2, v1, v2);
    v1.operator=(1);
    v2.operator=("A");
    result = std::operator<(v1, v2);
    std::visit(print2, v1, v2);
    v1.operator=(v2.operator=("A"));
    result = std::operator<(v1, v2);
    std::visit(print2, v1, v2);
    v2.operator=("B");
    result = std::operator<(v1, v2);
    std::visit(print2, v1, v2);
    v1.operator=("C");
    result = std::operator<(v1, v2);
    std::visit(print2, v1, v2);
  };
  {
    std::variant<int, std::basic_string<char> > v1 = std::variant<int, std::basic_string<char> >();
    std::variant<std::basic_string<char>, int> v2 = std::variant<std::basic_string<char>, int>();
  };
  return 0;
}

move语义

只要所有的选项都支持move语义,那么std::variant<>也支持move语义。
如果你movevariant对象,那么当前状态会被拷贝,而当前选项的值会被move。因此,被movevariant对象仍然保持之前的选项,但值会变为未定义
你也可以把值移进或移出variant对象。

哈希

如果所有的选项类型都能计算哈希值,那么variant对象也能计算哈希值。注意variant对象的哈希值 保证是当前选项的哈希值。在某些平台上它是,有些平台上不是。

访问器

另一个处理variant对象的值的方法是使用访问器(visitor)。访问器是为每一个可能的类型都提供一个函数调用运算符的对象。当这些对象“访问”一个variant时,它们会调用和当前选项类型最匹配的函数

使用函数对象作为访问器

例如:

#include <iostream>
#include <string>
#include <variant>

struct MyVisitor {
    void operator()(int i) const { std::cout << "int:    " << i << '\n'; }
    void operator()(std::string s) const {
        std::cout << "string: " << s << '\n';
    }
    void operator()(long double d) const {
        std::cout << "double: " << d << '\n';
    }
};
int main() {
    std::variant<int, std::string, long double> var(42);
    std::visit(MyVisitor(), var);  // 调用int的operator()
    var = "hello";
    std::visit(MyVisitor(), var);  // 调用string的operator()
    var = 42.7;
    std::visit(MyVisitor(), var);  // 调用long double的operator()
}

预处理代码如下:

struct MyVisitor
{
  inline void operator()(int i) const
  {
    std::operator<<(std::operator<<(std::cout, "int:    ").operator<<(i), '\n');
  }
  
  inline void operator()(std::basic_string<char> s) const
  {
    std::operator<<(std::operator<<(std::operator<<(std::cout, "string: "), s), '\n');
  }
  
  inline void operator()(long double d) const
  {
    std::operator<<(std::operator<<(std::cout, "double: ").operator<<(d), '\n');
  } 
};

int main()
{
  std::variant<int, std::basic_string<char>, long double> var = std::variant<int, std::basic_string<char>, long double>(42);
  std::visit(MyVisitor(), var);
  var.operator=("hello");
  std::visit(MyVisitor(), var);
  var.operator=(42.700000000000003);
  std::visit(MyVisitor(), var);
  return 0;
}

如果访问器没有某一个可能的类型的operator()重载,那么调用visit()将会导致编译期错误,如果调用有歧义的话也会导致编译期错误。
例如:std::variant<int, std::string, double> var(42);代码将无法通过编译。

....
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/variant:1032:31: error: no matching function for call to '__invoke(MyVisitor, double&)'
 1032 |           return std::__invoke(std::forward<_Visitor>(__visitor),
      |                  ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 1033 |               __element_by_index_or_cookie<__indices>(
      |               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 1034 |                 std::forward<_Variants>(__vars))...);
      |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
....

你也可以使用访问器来修改当前选项的值(但不能赋予一个其他选项的新值)。例如:

#include <iostream>
#include <string>
#include <variant>
using namespace std;
struct Twice {
    void operator()(double& d) const { d *= 2; }
    void operator()(int& i) const { i *= 2; }
    void operator()(std::string& s) const { s = s + s; }
};
int main() {
    std::variant<int, std::string, double> var(42);
    std::visit(Twice(), var);  // 调用匹配类型的operator()
    cout << std::get<0>(var);
}

访问器调用时只根据类型判断,你不能对类型相同的不同选项做不同的处理

注意上面例子中的函数调用运算符都应该标记为const,因为它们是 无状态的(stateless) (它们的行为只受参数的影响)。

使用泛型lambda作为访问器

最简单的使用访问器的方式是使用泛型lambda,它是一个可以处理任意类型的函数对象

#include <iostream>
#include <string>
#include <variant>

using namespace std;

int main() {
    auto pp = [] (const auto& val) {
                        std::cout << val << '\n';
                    };
    std::variant<int> var(42);
    std::visit( pp, var);
}

这里,泛型lambda生成的闭包类型中会将函数调用运算符定义为模板

class ComplierSpecificClosureTypeName {
public:
    template<typename T>
    auto operator() (const T& val) const {
        std::cout << val << '\n';
    }
};

因此,只要调用时生成的函数内的语句有效(这个例子中就是输出运算符要有效),那么把lambda传递给std::visit()就可以正常编译。
预处理代码如下:

#include <iostream>
#include <string>
#include <variant>

using namespace std;

int main()
{
    
  class __lambda_8_15
  {
    public: 
    template<class type_parameter_0_0>
    inline /*constexpr */ auto operator()(const type_parameter_0_0 & val) const
    {
      operator<<(operator<<(std::cout, val), '\n');
    }
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ void operator()<int>(const int & val) const
    {
      std::operator<<(std::cout.operator<<(val), '\n');
    }
    #endif
    
    private: 
    template<class type_parameter_0_0>
    static inline /*constexpr */ auto __invoke(const type_parameter_0_0 & val)
    {
      return __lambda_8_15{}.operator()<type_parameter_0_0>(val);
    }
    
  };
  
  __lambda_8_15 pp = __lambda_8_15{};
  std::variant<int> var = std::variant<int>(42);
  std::visit(pp, var);
  return 0;
}

你也可以使用lambda来修改当前选项的值:

#include <iostream>
#include <string>
#include <variant>

using namespace std;

int main() {
    auto pp = [](auto& val) {
        std::cout << val << '\n'
        val = val + val;
    };
    std::variant<int> var(42);
    std::visit(pp, var);//输出42
    cout << std::get<0>(var) << endl; // 输出84
    // 将当前选项的值变为两倍:
    std::visit([](auto& val) { val = val + val; }, var);
    cout << std::get<0>(var) << endl;//输出168
}

或者:

#include <iostream>
#include <string>
#include <variant>

using namespace std;

int main() {
    std::variant<int> var(42);
    // 将当前选项的值设为默认值
    std::visit(
        [](auto& val) { val = std::remove_reference_t<decltype(val)>{}; }, var);
    cout << std::get<0>(var) << endl; //输出0
}

你甚至可以使用编译期if语句来对不同的选项类型进行不同的处理。例如:

#include <iostream>
#include <string>
#include <variant>

using namespace std;

int main() {
    auto dblvar = [](auto& val) {
        if constexpr (std::is_convertible_v<decltype(val), std::string>) {
            val = val + val;
        } else {
            val *= 2;
        }
    };
    std::variant<int> var(42);
    std::visit(dblvar, var);
    cout << std::get<0>(var) << endl;
}

预处理代码如下:

using namespace std;

int main()
{
    
  class __lambda_8_19
  {
    public: 
    template<class type_parameter_0_0>
    inline /*constexpr */ auto operator()(type_parameter_0_0 & val) const
    {
      if constexpr(std::is_convertible_v<decltype(val), std::basic_string<char> >) {
        val = operator+(val, val);
      } else /* constexpr */ {
        val = static_cast<type_parameter_0_0>(static_cast<<dependent type>>(val) * 2);
      } 
      
    }
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ void operator()<int>(int & val) const
    {
      if constexpr(false) {
      } else /* constexpr */ {
        val = static_cast<int>(val * 2);
      } 
      
    }
    #endif
    
    private: 
    template<class type_parameter_0_0>
    static inline /*constexpr */ auto __invoke(type_parameter_0_0 & val)
    {
      return __lambda_8_19{}.operator()<type_parameter_0_0>(val);
    }
    
  };
  __lambda_8_19 dblvar = __lambda_8_19{};
  std::variant<int> var = std::variant<int>(42);
  std::visit(dblvar, var);
  std::cout.operator<<(std::get<0>(var)).operator<<(std::endl);
  return 0;
}

这里,对于std::string类型的选项,泛型lambda会把函数调用模板实例化为计算:

val = val + val;

而对于其他类型的选项,例如intdouble,lambda函数调用模板会实例化为计算:

val *= 2;

注意检查val的类型时必须小心。这里,我们检查了val的类型是否能转换为std::string。如下检查:

if constexpr(std::is_same_v<decltype(val), std::string>) {

将不能正确工作,因为val的类型只可能是int&、std::string&、long double&这样的引用类型。

在访问器中返回值

访问器中的函数调用可以返回值,但所有返回值类型必须相同。例如:

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

using namespace std;

int main() {
    using IntOrDouble = std::variant<int, double>;

    std::vector<IntOrDouble> coll{42, 7.7, 0, -0.7};

    double sum{0};
    for (const auto& elem : coll) {
        sum += std::visit(
            [](const auto& val) -> double {
                cout << val << endl;
                return val;
            },
            elem);
    }
    cout << sum << endl;
}

输出结果如下:

42
7.7
0
-0.7
49

上面的代码会把所有选项的值加到sum上。如果lambda没有显式指明返回类型将不能通过编译,因为自动推导的话返回类型会不同。

使用重载的lambda作为访问器

通过使用函数对象和lambda重载器(overloader) ,可以定义一系列lambda,其中最佳的匹配将会被用作访问器

假设有一个如下定义的overload重载器:

// 继承所有基类里的函数调用运算符
template<typename... Ts>
struct overload : Ts...
{
    using Ts::operator()...;
};

// 基类的类型从传入的参数中推导
template<typename... Ts>
overload(Ts...) -> overload<Ts...>;

你可以为每个可能的选项提供一个lambda,之后使用overload来访问variant

std::variant<int, std::string> var(42);
...
std::visit(overload { // 为当前选项调用最佳匹配的lambda
               [](int i) { std::cout << "int: " << i << '\n'; },
               [] (const std::string& s) {
               std::cout << "string: " << s << '\n';
           },
       }, var);

你也可以使用泛型lambda,它可以匹配所有情况。例如,要想修改一个variant当前选项的值,你可以使用重载实现字符串和其他类型值“翻倍”:

auto twice = overload {
                 [] (std::string& s) { s += s; },
                 [] (auto& i) { i *= 2; },
             };

使用这个重载,对于字符串类型的选项,值将变为原本的字符串再重复一遍;而对于其他类型,将会把值乘以2。下面展示了怎么应用于variant

std::variant<int, std::string> var(42);
std::visit(twice, var); // 值42变为84
...
var = "hi";
std::visit(twice, var); // 值"hi"变为"hihi"

示例:

// 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';
  
}

运行结果


int
char
unknown type
long long int
long int
unknown type
int

1.5
1 2 3 4 5 
Hello 

预处理代码如下:

// visitVariantsOverloadPattern.cpp

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

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

/* First instantiated from: insights.cpp:22 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> : public __lambda_23_9, public __lambda_24_9, public __lambda_25_9, public __lambda_26_9, public __lambda_27_9, public __lambda_28_9
{
  using __lambda_23_9::operator();
  // inline /*constexpr */ const char * ::operator()(char) const;
  
  using __lambda_24_9::operator();
  // inline /*constexpr */ const char * ::operator()(int) const;
  
  using __lambda_25_9::operator();
  // inline /*constexpr */ const char * ::operator()(unsigned int) const;
  
  using __lambda_26_9::operator();
  // inline /*constexpr */ const char * ::operator()(long) const;
  
  using __lambda_27_9::operator();
  // inline /*constexpr */ const char * ::operator()(long long) const;
  
  using __lambda_28_9::operator();
  template<class type_parameter_0_0>
  inline /*constexpr */ auto ::operator()(type_parameter_0_0) const
  {
    return "unknown type";
  }
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ auto ::operator()<char>(char) const;
  #endif
  
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ auto ::operator()<long>(long) const;
  #endif
  
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ const char * ::operator()<float>(float) const
  {
    return "unknown type";
  }
  #endif
  
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ auto ::operator()<int>(int) const;
  #endif
  
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ const char * ::operator()<double>(double) const
  {
    return "unknown type";
  }
  #endif
  
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ auto ::operator()<long long>(long long) const;
  #endif
  
  
  // inline constexpr Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> & operator=(const Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> &) /* noexcept */ = delete;
  // inline constexpr Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> & operator=(Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> &&) /* noexcept */ = delete;
};

#endif
/* First instantiated from: insights.cpp:40 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Overload<__lambda_41_9, __lambda_45_9> : public __lambda_41_9, public __lambda_45_9
{
  using __lambda_41_9::operator();
  // inline /*constexpr */ void ::operator()(std::vector<int, std::allocator<int> > & myVec) const;
  
  using __lambda_45_9::operator();
  template<class type_parameter_0_0>
  inline /*constexpr */ auto ::operator()(type_parameter_0_0 & arg) const
  {
    (std::cout << arg) << '\n';
  }
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ auto ::operator()<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> > & arg) const;
  #endif
  
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ void ::operator()<double>(double & arg) const
  {
    std::operator<<(std::cout.operator<<(arg), '\n');
  }
  #endif
  
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ void ::operator()<std::basic_string<char> >(std::basic_string<char> & arg) const
  {
    std::operator<<(std::operator<<(std::cout, arg), '\n');
  }
  #endif
  
  
  // inline constexpr Overload<__lambda_41_9, __lambda_45_9> & operator=(const Overload<__lambda_41_9, __lambda_45_9> &) /* noexcept */ = delete;
  // inline constexpr Overload<__lambda_41_9, __lambda_45_9> & operator=(Overload<__lambda_41_9, __lambda_45_9> &&) /* noexcept */ = delete;
};

#endif

template<class... Ts> Overload(Ts...) -> Overload<Ts...>;


/* First instantiated from: insights.cpp:22 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Overload(__lambda_23_9 __0, __lambda_24_9 __1, __lambda_25_9 __2, __lambda_26_9 __3, __lambda_27_9 __4, __lambda_28_9 __5) -> Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9>;
#endif


/* First instantiated from: insights.cpp:40 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Overload(__lambda_41_9 __0, __lambda_45_9 __1) -> Overload<__lambda_41_9, __lambda_45_9>;
#endif

int main()
{
  std::operator<<(std::cout, '\n');
  std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > > vecVariant = std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > >{std::initializer_list<std::variant<char, long, float, int, double, long long> >{std::variant<char, long, float, int, double, long long>(5), std::variant<char, long, float, int, double, long long>('2'), std::variant<char, long, float, int, double, long long>(5.4000000000000004), std::variant<char, long, float, int, double, long long>(100LL), std::variant<char, long, float, int, double, long long>(2011L), std::variant<char, long, float, int, double, long long>(3.5F), std::variant<char, long, float, int, double, long long>(2017)}, std::allocator<std::variant<char, long, float, int, double, long long> >()};
    
  class __lambda_23_9
  {
    public: 
    inline /*constexpr */ const char * operator()(char) const
    {
      return "char";
    }
    
    using retType_23_9 = const char *(*)(char);
    inline constexpr operator retType_23_9 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline /*constexpr */ const char * __invoke(char __param0)
    {
      return __lambda_23_9{}.operator()(__param0);
    }
    
    public: 
    // inline /*constexpr */ __lambda_23_9 & operator=(const __lambda_23_9 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_23_9(__lambda_23_9 &&) noexcept = default;
    
  };
  
  
  class __lambda_24_9
  {
    public: 
    inline /*constexpr */ const char * operator()(int) const
    {
      return "int";
    }
    
    using retType_24_9 = const char *(*)(int);
    inline constexpr operator retType_24_9 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline /*constexpr */ const char * __invoke(int __param0)
    {
      return __lambda_24_9{}.operator()(__param0);
    }
    
    public: 
    // inline /*constexpr */ __lambda_24_9 & operator=(const __lambda_24_9 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_24_9(__lambda_24_9 &&) noexcept = default;
    
  };
  
  
  class __lambda_25_9
  {
    public: 
    inline /*constexpr */ const char * operator()(unsigned int) const
    {
      return "unsigned int";
    }
    
    using retType_25_9 = const char *(*)(unsigned int);
    inline constexpr operator retType_25_9 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline /*constexpr */ const char * __invoke(unsigned int __param0)
    {
      return __lambda_25_9{}.operator()(__param0);
    }
    
    public: 
    // inline /*constexpr */ __lambda_25_9 & operator=(const __lambda_25_9 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_25_9(__lambda_25_9 &&) noexcept = default;
    
  };
  
  
  class __lambda_26_9
  {
    public: 
    inline /*constexpr */ const char * operator()(long) const
    {
      return "long int";
    }
    
    using retType_26_9 = const char *(*)(long);
    inline constexpr operator retType_26_9 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline /*constexpr */ const char * __invoke(long __param0)
    {
      return __lambda_26_9{}.operator()(__param0);
    }
    
    public: 
    // inline /*constexpr */ __lambda_26_9 & operator=(const __lambda_26_9 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_26_9(__lambda_26_9 &&) noexcept = default;
    
  };
  
  
  class __lambda_27_9
  {
    public: 
    inline /*constexpr */ const char * operator()(long long) const
    {
      return "long long int";
    }
    
    using retType_27_9 = const char *(*)(long long);
    inline constexpr operator retType_27_9 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline /*constexpr */ const char * __invoke(long long __param0)
    {
      return __lambda_27_9{}.operator()(__param0);
    }
    
    public: 
    // inline /*constexpr */ __lambda_27_9 & operator=(const __lambda_27_9 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_27_9(__lambda_27_9 &&) noexcept = default;
    
  };
  
  
  class __lambda_28_9
  {
    public: 
    template<class type_parameter_0_0>
    inline /*constexpr */ auto operator()(type_parameter_0_0) const
    {
      return "unknown type";
    }
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ auto operator()<char>(char) const;
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ auto operator()<long>(long) const;
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ const char * operator()<float>(float) const
    {
      return "unknown type";
    }
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ auto operator()<int>(int) const;
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ const char * operator()<double>(double) const
    {
      return "unknown type";
    }
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ auto operator()<long long>(long long) const;
    #endif
    
    private: 
    template<class type_parameter_0_0>
    static inline /*constexpr */ auto __invoke(type_parameter_0_0 __param0)
    {
      return __lambda_28_9{}.operator()<type_parameter_0_0>(__param0);
    }
    public: 
    // inline /*constexpr */ __lambda_28_9 & operator=(const __lambda_28_9 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_28_9(__lambda_28_9 &&) noexcept = default;
    
  };
  
  Overload<__lambda_23_9, __lambda_24_9, __lambda_25_9, __lambda_26_9, __lambda_27_9, __lambda_28_9> TypeOfIntegral = Overload{__lambda_23_9(__lambda_23_9{}), __lambda_24_9(__lambda_24_9{}), __lambda_25_9(__lambda_25_9{}), __lambda_26_9(__lambda_26_9{}), __lambda_27_9(__lambda_27_9{}), __lambda_28_9(__lambda_28_9{})};
  {
    std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > > & __range1 = vecVariant;
    __gnu_cxx::__normal_iterator<std::variant<char, long, float, int, double, long long> *, std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > > > __begin1 = __range1.begin();
    __gnu_cxx::__normal_iterator<std::variant<char, long, float, int, double, long long> *, std::vector<std::variant<char, long, float, int, double, long long>, std::allocator<std::variant<char, long, float, int, double, long long> > > > __end1 = __range1.end();
    for(; __gnu_cxx::operator!=(__begin1, __end1); __begin1.operator++()) {
      std::variant<char, long, float, int, double, long long> v = std::variant<char, long, float, int, double, long long>(__begin1.operator*());
      std::operator<<(std::operator<<(std::cout, std::visit(TypeOfIntegral, v)), '\n');
    }
    
  }
  std::operator<<(std::cout, '\n');
  std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > > vecVariant2 = std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > >{std::initializer_list<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > >{std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >(1.5), std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >(std::vector<int, std::allocator<int> >{std::initializer_list<int>{1, 2, 3, 4, 5}, std::allocator<int>()}), std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >("Hello ")}, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > >()};
    
  class __lambda_41_9
  {
    public: 
    inline /*constexpr */ void operator()(std::vector<int, std::allocator<int> > & myVec) const
    {
      {
        std::vector<int, std::allocator<int> > & __range1 = myVec;
        __gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __begin1 = __range1.begin();
        __gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __end1 = __range1.end();
        for(; __gnu_cxx::operator!=(__begin1, __end1); __begin1.operator++()) {
          int v = __begin1.operator*();
          std::operator<<(std::cout.operator<<(v), " ");
        }
        
      }
      std::operator<<(std::cout, '\n');
    }
    
    using retType_41_9 = void (*)(std::vector<int> &);
    inline constexpr operator retType_41_9 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline /*constexpr */ void __invoke(std::vector<int, std::allocator<int> > & myVec)
    {
      __lambda_41_9{}.operator()(myVec);
    }
    
    public: 
    // inline /*constexpr */ __lambda_41_9 & operator=(const __lambda_41_9 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_41_9(__lambda_41_9 &&) noexcept = default;
    
  };
  
  
  class __lambda_45_9
  {
    public: 
    template<class type_parameter_0_0>
    inline /*constexpr */ auto operator()(type_parameter_0_0 & arg) const
    {
      (std::cout << arg) << '\n';
    }
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ auto operator()<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> > & arg) const;
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ void operator()<double>(double & arg) const
    {
      std::operator<<(std::cout.operator<<(arg), '\n');
    }
    #endif
    
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ void operator()<std::basic_string<char> >(std::basic_string<char> & arg) const
    {
      std::operator<<(std::operator<<(std::cout, arg), '\n');
    }
    #endif
    
    private: 
    template<class type_parameter_0_0>
    static inline /*constexpr */ auto __invoke(type_parameter_0_0 & arg)
    {
      return __lambda_45_9{}.operator()<type_parameter_0_0>(arg);
    }
    public: 
    // inline /*constexpr */ __lambda_45_9 & operator=(const __lambda_45_9 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_45_9(__lambda_45_9 &&) noexcept = default;
    
  };
  
  Overload<__lambda_41_9, __lambda_45_9> DisplayMe = Overload{__lambda_41_9(__lambda_41_9{}), __lambda_45_9(__lambda_45_9{})};
  {
    std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > > & __range1 = vecVariant2;
    __gnu_cxx::__normal_iterator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > *, std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > > > __begin1 = __range1.begin();
    __gnu_cxx::__normal_iterator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > *, std::vector<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >, std::allocator<std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > > > > __end1 = __range1.end();
    for(; __gnu_cxx::operator!=(__begin1, __end1); __begin1.operator++()) {
      std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> > v = std::variant<std::vector<int, std::allocator<int> >, double, std::basic_string<char> >(__begin1.operator*());
      std::visit(DisplayMe, v);
    }
    
  }
  std::operator<<(std::cout, '\n');
  return 0;
}

异常造成的无值

如果你赋给一个variant新值时发生了异常,那么这个variant可能会进入一个非常特殊的状态:它已经失去了旧的值但还没有获得新的值。例如:

struct S {
    operator int() { throw "EXCEPTION"; } // 转换为int时会抛出异常
};
std::variant<double, int> var{12.2};      // 初始化为double
var.emplace<1>(S{});    // OOPS:当设为int时抛出异常

如果这种情况发生了,那么:

  • var.valueless_by_exception()会返回true
  • var.index()会返回std::variant_npos

这些都标志该variant当前没有值
这种情况下有如下保证:

  • 如果emplace()抛出异常,那么valueless_by_exception()可能会返回true
  • 如果operator=()抛出异常且该修改不会改变选项,那么index()valueless_by_exception()的状态将保持不变。值的状态依赖于值类型的异常保证。
  • 如果operator=()抛出异常且新值是新的选项,那么variant 可能 会没有值(valueless_by_exception() 可能 会返回true)。具体情况依赖于异常抛出的时机。如果发生在实际修改值之前的类型转换期间,那么variant将依然持有旧值。
    通常情况下,如果你不再使用这种情况下的variant,那么这些保证就足够了。如果你仍然想使用抛出了异常的variant,你需要检查它的状态。例如:
std::variant<double, int> var{12.2};  // 初始化为double
try {
    var.emplace<1>(S{});              // OOPS:设置为int时抛出异常
}
catch (...) {
    if (!var.valueless_by_exception()) {
        ...
    }
}

使用std::variant实现多态的异质集合

std::variant允许一种新式的多态性,可以用来实现异质集合。这是一种带有闭类型集合的运行时多态性

关键在于variant<>可以持有多种选项类型的值。可以将元素类型定义为variant来实现异质的集合,这样的集合可以持有不同类型的值。因为每一个variant知道当前的选项,并且有了访问器接口,我们可以定义在运行时根据不同类型进行不同操作的函数/方法。同时因为variant有值语义,所以我们不需要指针(和相应的内存管理)或者虚函数。

使用std::variant实现几何对象

例如,假设我们要负责编写表示几何对象的库:

#include <iostream>
#include <variant>
#include <vector>
#include "coord.hpp"
#include "line.hpp"
#include "circle.hpp"
#include "rectangle.hpp"

// 所有几何类型的公共类型
using GeoObj = std::variant<Line, Circle, Rectangle>;

// 创建并初始化一个几何体对象的集合
std::vector<GeoObj> createFigure()
{
    std::vector<GeoObj> f;
    f.push_back(Line{Coord{1, 2}, Coord{3, 4}});
    f.push_back(Circle{Coord{5, 5}, 2});
    f.push_back(Rectangle{Coord{3, 3}, Coord{6, 4}});
    return f;
}

int main()
{
    std::vector<GeoObj> figure = createFigure();
    for (const GeoObj& geoobj : figure) {
        std::visit([] (const auto& obj) {
            obj.draw(); // 多态性调用draw()
        }, geoobj);
    }
}

首先,我们为所有可能的类型定义了一个公共类型:

using GeoObj = std::variant<Line, Circle, Rectangle>;

这三个类型不需要有任何特殊的关系。事实上它们甚至没有一个公共的基类、没有任何虚函数、接口也可能不同。例如:

#ifndef CIRCLE_HPP
#define CIRCLE_HPP

#include "coord.hpp"
#include <iostream>

class Circle {
private:
    Coord center;
    int rad;
public:
    Circle (Coord c, int r) : center{c}, rad{r} {
    }

    void move(const Coord& c) {
        center += c;
    }

    void draw() const {
        std::cout << "circle at " << center << " with radius " << rad << '\n';
    }
};

#endif

我们现在可以创建相应的对象并把它们以值传递给容器,最后可以得到这些类型的元素的集合:

std::vector<GeoObj> createFigure()
{
    std::vector<GeoObj> f;
    f.push_back(Line{Coord{1, 2}, Coord{3, 4}});
    f.push_back(Circle{Coord{5, 5}, 2});
    f.push_back(Rectangle{Coord{3, 3}, Coord{6, 4}});
    return f;
}

以前如果没有使用继承和多态的话是不可能写出这样的代码的。以前要想实现这样的异构集合,所有的类型都必须继承自GeoObj,并且最后将得到一个元素类型为GeoObj的指针的vector。为了使用指针,必须用new创建新对象,这导致最后还要追踪什么时候调用delete,或者要使用智能指针来完成(unique_ptr或者shared_ptr)。
现在,通过使用访问器,我们可以迭代每一个元素,并依据元素的类型“做正确的事情”:

std::vector<GeoObj> figure = createFigure();
for (const GeoObj& geoobj : figure) {
    std::visit([] (const auto& obj) {
                   obj.draw();  // 多态调用draw()
               }, geoobj);
}

这里,visit()使用了泛型lambda来为每一个可能的GeoObj类型实例化。也就是说,当编译visit()调用时,lambda将会被实例化并编译为3个函数:

  • 为类型Line编译代码:
[] (const Line& obj) {
    obj.draw(); // 调用Line::draw()
}
  • 为类型Circle编译代码:
[] (const Circle& obj) {
    obj.draw(); // 调用Circle::draw()
}
  • 为类型Rectangle编译代码:
[] (const Rectangle& obj) {
    obj.draw(); // 调用Rectangle::draw()
}

如果这些实例中有一个不能编译,那么对visit()的调用也不能编译。如果所有实例都能编译,那么将保证会对所有元素类型调用相应的函数。注意生成的代码并不是 if-else 链。C++标准保证这些调用的性能不会依赖于variant选项的数量。

也就是说,从效率上讲,这种方式和虚函数表的方式的行为相同(通过类似于为所有visit()创建局部虚函数表的方式)。注意,draw()函数不需要是虚函数。
如果对不同类型的操作不同,我们可以使用编译期if语句或者重载访问器来处理不同的情况

使用std::variant实现其他异质集合

考虑如下另一个使用std::variant<>实现异质集合的例子:

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

int main()
{
    using Var = std::variant<int, double, std::string>;

    std::vector<Var> values {42, 0.19, "hello world", 0.815};

    for (const Var& val : values) {
        std::visit([] (const auto& v) {
            if constexpr(std::is_same_v<decltype(v), const std::string&>) {
                std::cout << '"' << v << "\" ";
            }
            else {
                std::cout << v << ' ';
            }
        }, val);
    }
}

我们又一次定义了自己的类型来表示若干可能类型中的一个:

using Var = std::variant<int, double, std::string>;

我们可以用它创建并初始化一个异质的集合:

std::vector<Var> values {42, 0.19, "hello world", 0.815};

注意我们可以用若干异质的元素来实例化vector,因为它们都能自动转换为variant类型。然而,如果我们还传递了一个long类型的初值,上面的初始化将不能编译,因为编译器不能决定将它转换为int还是double

当我们迭代元素时,我们使用了访问器来调用相应的函数。这里使用了一个泛型lambda。lambda为3种可能的类型分别实例化了一个函数调用。为了对字符串进行特殊的处理(在输出值时用双引号包括起来),我们使用了编译期if语句:

for (const Var& val : values) {
    std::visit([] (const auto& v) {
        if constexpr(std::is_same_v<decltype(v), const std::string&>) {
            std::cout << '"' << v << "\" ";
        }
        else {
            std::cout << v << ' ';
        }
    }, val);
}

这意味着输出将是:

42 0.19 "hello world" 0.815

通过使用重载的访问器,我们可以像下面这样实现:

for (const auto& val : values) {
    std::visit(overload {
        [] (const auto& v) {
            std::cout << v << ' ';
        },
        [] (const std::string& v) {
            std::cout << '"' << v "\" ";
        }
    }, val);
}

然而,注意这样可能会陷入重载匹配的问题。有的情况下泛型lambda(即函数模板)匹配度比隐式类型更高,这意味着可能会调用错误的类型。

比较多态的variant

让我们来总结一下使用std::variant实现多态的异构集合的优点和缺点:

优点:

  • 你可以使用任意类型并且这些类型不需要有公共的基类(这种方法是非侵入性的)
  • 你不需要使用指针来实现异质集合
  • 不需要virtual成员函数,不需要虚函数表,因此开销更小
  • 值语义,无需动态分配内存,也不需要unique 或者 智能指针(不会出现访问已释放内存或内存泄露等问题)
  • vector中的元素是连续存放在一起的(原本指针的方式所有元素是散乱分布在堆内存中的)

缺点:

  • 闭类型集合(你必须在编译期指定所有可能的类型
  • 每个元素的大小都是所有可能的类型中最大的(当不同类型大小差距很大时这是个问题)
  • 拷贝元素的开销可能会更大

一般来说,我并不确定是否要推荐默认使用std::variant<>来实现多态。一方面这种方法很安全(没有指针,意味着没有newdelete),也不需要虚函数。然而另一方面,使用访问器有一些笨拙,有时你可能会需要引用语义(在多个地方使用同一个对象),还有在某些情形下并不能在编译期确定所有的类型。
性能开销也有很大不同。没有了newdelete可能会减少很大开销但另一方面,以值传递对象又可能会增大很多开销。在实践中,你必须自己测试对你的代码来说哪种方法效率更高。

特殊情况

特定类型的variant可能导致特殊或者出乎意料的行为。

同时有boolstd::string选项

如果一个std::variant<>同时有boolstd::string选项,赋予一个字符串字面量可能会导致令人惊奇的事,因为字符串字面量会优先转换为bool,而不是std::string。例如:

std::variant<bool, std::string> v;
v = "hi";   // OOPS:设置bool选项
std::cout << "index: " << v.index() << '\n';
std::visit([] (const auto& val) {
               std::cout << "value: " << val << '\n';
           }, v);

这段代码片段将会有如下输出:

index: 0
value: true

可以看出,字符串字面量会被解释为把variant的bool选项初始化为true(因为指针不是0所以是true)。
这里有一些修正这个赋值问题的方法:

v.emplace<1>("hello");           // 显式赋值给第二个选项

v.emplace<std::string>("hello"); // 显式赋值给string选项

v = std::string{"hello"};        // 确保用string赋值

using namespace std::literals;   // 确保用string赋值
v = "hello"s;

参考

[1] Replacing Unique_ptr With C++17’s std::variant — a Practical Experiment
[2] Visiting a std::variant with the Overload Pattern
[3] std::variant
[4] Modern C++ Features – std::variant and std::visit
[5] Everything You Need to Know About std::variant from C++17

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-西门吹雪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值