C++17之std::variant

    从C中采用的c++提供了对union的支持,union是能够保存可能类型列表之一的对象。但是,这种语言特性也有一些缺点:

  • 对象不知道它们当前持有的值的类型。
  • 由于这个原因,您不能有non-trivial的成员,比如std::string(从c++ 11起, union原则上可以有non-trivial的成员,但是必须实现特殊的成员函数,比如复制构造函数和析构函数,因为只有通过代码逻辑才能知道哪个成员是可用的。)
  • 不能从union中派生类。

     对于std:: variable <>, c++标准库提供了一个封闭的区分联合(这意味着有一个指定的可能类型列表,可以指定你要指的是哪种类型),其中:

  • 当前值的类型总是已知的;
  • 可以有任何指定类型的成员;
  • 可以派生类。

事实上,一个std:: variable <>拥有不同的替代值,这些替代值通常具有不同的类型。与std::optional<>和std::any一样,生成的任何对象都具有值语义。也就是说,通过在它自己的内存中创建一个具有当前替代的当前值的独立对象来进行深度复制。因此,复制std:: variable <>与复制当前替代的类型/值一样便宜/昂贵。支持Move语义。

1.1 使用std::variant

下面的例子演示了std:: variable <>的核心功能:

#include 

std::variant var{"hi"}; // initialized with string alternative
std::cout << var.index(); // prints 1
var = 42; // now holds int alternative
std::cout << var.index(); // prints 0
...
try {
std::string s = std::get(var); // access by type
int i = std::get<0>(var); // access by index
}
catch (const std::bad_variant_access& e) { // in case a wrong type/index is used
...
}

    成员函数index()可用于查明当前设置了哪个选项(第一个选项的索引为0)。初始化和赋值总是使用最佳匹配来找到新选项。如果类型不完全匹配,可能会出现意外。

    注意,不允许使用空变量、具有引用成员的变量、具有c样式数组成员的变量和具有不完整类型(如void)的变量。没有空的状态:这意味着对于每个构建的对象,必须至少调用一个构造函数。默认构造函数初始化第一个类型(通过第一个类型的默认构造函数):

std::variant var; // => var.index() == 0, value == ””

如果没有为第一个类型定义默认构造函数,则调用该变量的默认构造函数会在编译时错误:

例1:

#include 
#include 

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

int main()
{
    std::variant v1; // ERROR: can’t default construct first type

    return 0;
}

编译错误如下:

不过辅助类型std::monostate提供了处理这种情况的能力,还提供了模拟空状态的能力。

1.1.1 std::monostate

为了支持第一个类型没有默认构造函数的variant对象,提供了一个特殊的helper类型:std::monostate。类型std::monostate的对象总是具有相同的状态,因此,它们总是相等的。它自己的目的是表示另一种类型,这样variant就没有任何其他类型的值。也就是说,std::monostate可以作为第一种替代类型,使变体类型默认为可构造的。例如:

std::variant v2; // OK
std::cout << "index: " << v2.index() << '\n'; // prints 0

在某种程度上,你可以把这种状态解释为模拟的(原则上,std::monostate可以作为任何替代,而不仅仅是第一个替代,当然,这个替代不能帮助使变体的默认构造成为可构造的)。

有多种方法可以检查monostate,这也演示了一些其他的操作,你可以调用变量:

例2:

#include 
#include 

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

int main()
{
    std::variant v2; // OK
    std::cout << "index: " << v2.index() << '\n'; // prints 0

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

    return 0;
}

结果如下:

C++17之std::variant_第1张图片

get_if < T>()使用一个指向一个variant的指针,如果当前的选项是T,返回一个指向当前选项的指针,否则它将返回nullptr。这与get()不同,get()接受对变量的引用,如果提供的类型正确,则按值返回当前替代,否则抛出异常。和往常一样,您可以为另一个选项赋值,甚至可以为monostate赋值,再次表示为空:

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

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

1.1.2 variant的派生类

可以从std::variant派生出子类。例如,可以定义一个从std:: variable <>派生的聚合,如下所示:

例3:

include 
#include 

class Derived : public std::variant 
{
};

int main()
{
    Derived d = { {"hello"} };
    std::cout << d.index() << '\n'; // prints: 1
    std::cout << std::get<1>(d) << '\n'; // prints: hello
    d.emplace<0>(77); // initializes int, destroys string
    std::cout << d.index() << '\n'; // prints: 0
    std::cout << std::get<0>(d) << '\n'; // prints: 77

    return 0;
}

结果如下:

C++17之std::variant_第2张图片

1.2 std::variant<>的类型和操作

本节详细介绍std::variant<>的类型和操作。

1.2.1 std::variant<>的类型

在头文件< variable >中,c++标准库定义了类std:: variable <>,如下所示:
 

namespace std 
{
    template class variant;
}

    也就是说,std:: variable <>是一个可变参数类模板(c++ 11引入的一个特性,允许处理任意数量的类型)。

此外,定义了以下类型和对象:

  • 类型 std::variant_size   
  • 类型 std::variant_alternative 
  •  值 std::variant_npos   
  • 类型 std::monostate
  • 异常类std::bad_variant_access派生自std:: Exception。

1.2.2 std::variant操作

如下列出了为std:: variable <>提供的所有操作。

std::variant<> 操作
操作 说明
constructors 创建一个variant对象(可能调用底层类型的构造函数)
destructor 销毁一个variant对象
= 分配一个新值
emplace() 为具有类型T的备选项分配一个新值
emplace() 为索引Idx的备选项分配一个新值
valueless_by_exception() 返回该变量是否由于异常而没有值
index() 返回当前备选项的索引

swap()

交换两个对象的值
==, !=, <, <=, >, >= 比较variant对象
hash<> 函数对象类型来计算哈希值
holds_alternative() 返回类型T是否有值
get() 返回备选项类型为T的值或抛出异常(如果没有类型为T的值)
get() 返回备选项索引为idx的值或抛出异常(如果没有索引为idx的值)
get_if() 返回指向类型为T指针或返回nullptr(如果没有类型为T的值)
get_if() 返回指向索引Idx的指针或nullpt(如果没有索引为idx的值)
visit() 为当前备选项执行操作

1. 构造函数

默认情况下,变量的默认构造函数调用第一个备选项的默认构造函数:

std::variant v1; // sets first int to 0, index()==0

另一种方法是初始化值,这意味着对于基本类型,它是0、false还是nullptr。如果传递一个值进行初始化,则使用最佳匹配类型:

std::variant v2{42};
std::cout << v2.index() << '\n'; // prints 1

然而,如果两种类型匹配得同样,则调用是不明确的:

std::variant v3{42}; // ERROR: ambiguous
std::variant v4{42.3}; // ERROR: ambiguous
std::variant v5{42.3}; // OK
std::variant v6{"hello"}; // ERROR: ambiguous
std::variant v7{"hello"}; // OK
std::cout << v7.index() << '\n'; // prints 2

要传递多个值进行初始化,必须使用in_place_type或in_place_index标记:

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

当然,可以使用in_place_index标签来解决初始化过程中的歧义或匹配问题:

std::variant v12{std::in_place_index<1>, 77}; // init 2nd int
std::variant v13{std::in_place_index<1>, 77}; // init long, not int
std::cout << v13.index() << '\n'; // prints 1

甚至可以传递一个初始化器列表,后面跟着附加的参数:

// initialize variant with a set with lambda as sorting criterion:
auto sc = [] (int x, int y) 
{
    return std::abs(x) < std::abs(y);
};

std::variant,std::set> v14{std::in_place_index<1>, {4, 8, -7, -2, 0, 5}, sc};

不能对std:: variable <>使用类模板参数推导,而且不存在make_variable <>()便利函数(与std::optional<>和std::any不同)。两者都没有意义,因为变体的整个目标是处理多个替代方案。

2. 访问值

访问值的通常方法是调用get<>()获取对应的选项值。可以传递它的索引或者类型。例如:

std::variant var; // sets first int to 0, index()==0
auto a = std::get(var); // compile-time ERROR: no double
auto b = std::get<4>(var); // compile-time ERROR: no 4th alternative
auto c = std::get(var); // compile-time ERROR: int twice

try{
    auto s = std::get(var); // throws exception (first int currently set)
    auto i = std::get<0>(var); // OK, i==0
    auto j = std::get<1>(var); // throws exception (other int currently set)
}
catch (const std::bad_variant_access& e) { // in case of an invalid access
    std::cout << "Exception: " << e.what() << '\n';
}

也有一个API来访问该值的选项,检查它是否存在:

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

必须将variant变量的指针传递给get_if<>(),它要么返回指向当前值的指针,要么返回nullptr。注意,这里使用了if with initialize,它允许检查刚刚初始化的值。另一种访问不同选项值的方法是variant访问器(后续文章会介绍)。

3. 修改值

赋值和emplace()操作对应于初始化:

std::variant var; // sets first int to 0, index()==0
var = "hello"; // sets string, index()==2
var.emplace<1>(42); // sets second int, index()==1

还可以使用get<>()或get_if<>()来为当前选项值分配一个新值:

std::variant var; // sets first int to 0, index()==0
std::get<0>(var) = 77; // OK, because first int already set
std::get<1>(var) = 99; // throws exception (other int currently set)

if (auto p = std::get_if<1>(&var); p) { // if second int set
*p = 42; // modify it
}

修改不同备选项值的另一种方法是使用不同的访问者。

4. variant对象比较

对于两个类型相同的variant(相同的备选项和顺序),可以使用通常的比较运算符。运算符根据如下规则:

  • 对于都有值的两个variant对象,index小的对象大于index大的对象;
  • 对于两个都有值并且index也相等的两个variant对象,按照其相应类型的比较运算符比较。注意所有对象的std::monostate对象总是相等的;

例4:

#include 
#include 

int main()
{
    std::variant v1, v2{ "hello" }, v3{ 42 };
    std::variant v4;

    //v1 == v4 // COMPILE-TIME ERROR
    std::cout << std::boolalpha;
    std::cout << (v1 == v2) << std::endl;// yields false
    std::cout << (v1 < v2) << std::endl; // yields true
    std::cout << (v1 < v3) << std::endl; // yields true
    std::cout << (v2 < v3) << std::endl; // yields false
    std::cout << (v2 == v3) << std::endl; // yields false
    std::cout << (v2 > v3) << std::endl; // yields true

    v1 = "hello";
    std::cout << (v1 == v2) << std::endl;// yields true

    v2 = 41;
    std::cout << (v2 < v3) << std::endl; // yields true
    std::cout << std::noboolalpha;

    return 0;
}

5. 移动语义

变量<>也支持移动语义。如果将对象作为一个整体移动,则会复制状态并移动当前备选项的值。因此,一个从对象中移出的对象仍然有相同的选项值,但是任何值都是为定义行为。还可以将一个值移动到或移出所包含的对象。

6. hash

当且仅当每个成员类型都能提供哈希值时,才启用variant对象的哈希值。注意,哈希值不是当前备选项的哈希值。

1.2.3 异常处理

当修改一个vairant对象,使它得到一个新的值,这个修改抛出一个异常时,variant对象可以进入一个非常特殊的状态:这个variant对象已经失去了它的旧值,但是没有得到它的新值。例如:

struct S
{
    operator int() { throw "EXCEPTION"; } // any conversion to int throws
};

std::variant var{12.2}; // initialized as double
var.emplace<1>(S{}); // OOPS: throws while set as int

如果发生这种情况,则:

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

这表明该变量没有任何值。

例5:

#include 
#include 

struct S
{
    operator int() { throw "EXCEPTION"; } // any conversion to int throws
};

int main()
{
    std::variant var{ 12.2 }; // initialized as double

    std::cout << std::boolalpha << var.valueless_by_exception() << std::endl;

    try
    { 
        var.emplace<1>(S{}); // OOPS: throws while set as int
    }
    catch (...)
    {
        std::cout << var.valueless_by_exception() << std::endl;
        if (var.index() == std::variant_npos)
        {
            std::cout << "variant_npos" << std::endl;
        }
    }
    
    return 0;
}

运行结果如下:

C++17之std::variant_第3张图片

但是其他的一些保证如下:

  • 如果emplace()抛出异常,则valueless_by_exception()始终将其设置为true;
  • 如果操作符=抛出异常,不会修改variant对象的值,valueless_by_exception()
    and index()保持他们以前的状态。值的状态取决于值类型的异常保证。
  • 如果操作符=()抛出异常,并且有可能新值将设置一个不同的备选项,则variant可能不包含任何值(valueless_by_exception()可能变为true)。这取决于何时抛出异常。如果抛出异常发生在实际修改值之前的类型转换期间,变量仍将保持其旧值。则variant对象仍然保留其旧值。

例6:

#include 
#include 

struct S
{
    operator int() { throw "EXCEPTION"; } // any conversion to int throws
};

int main()
{
    std::variant var{ 12.2 }; // initialized as double

    std::cout << std::boolalpha << var.valueless_by_exception() << std::endl;

    try
    {
        var = S{}; // OOPS: throws while set as int
    }
    catch (...)
    {
        std::cout << var.valueless_by_exception() << std::endl;
        std::cout << "var.index()=" << var.index() << std::endl;
    }

    return 0;
}

 结果如下:

C++17之std::variant_第4张图片

通常,只要不再使用试图修改的variant对象,这种行为应该没有问题。如果你仍旧想使用variant变量(尽管是会用他引起异常),最好检查下variant的状态。例如:

std::variant var{12.2}; // initialized as double
try 
{
    var.emplace<1>(S{}); // OOPS: throws while set as int
}
catch (...) 
{
    if (!var.valueless_by_exception()) 
    {
        ...
    }
}

1.3 特定情况

特定的variant可能导致特殊或意外的行为。

1.3.1 同时具有bool和std::string选项

    如果一个std::variant<>同时有bool和std::string两个备选项,分配字符串字面量可能会变得令人惊讶,因为字符串字面量转换为bool比转换为std::string匹配。

例7:


#include 
#include 

int main()
{
    std::variant v;
    v = "hi"; // OOPS: sets the bool alternative
    std::cout << "index: " << v.index() << '\n';

    std::visit([](const auto& val) {std::cout << "value: " << val << '\n'; }, v);

    return 0;
}

结果如下:

 因此,字符串常量值被解释为通过Boolean值true初始化变量(true是因为指针不是0)。

对于上面存在的这个问题,这里有几个方法可以来修复:

v.emplace<1>("hello"); // explicitly assign to second alternative
v.emplace("hello"); // explicitly assign to string alternative
v = std::string{"hello"}; // make sure a string is assigned

using namespace std::literals; // make sure a string is assigned
v = "hello"s;

 

你可能感兴趣的:(C++17)