Standard Variant in C++

Do this at first: Unions in C++

std::variant: A Type-Safe Union

在联合体的小节中,我们谈到由于联合体的特性,每次只能有一个成员处于 active 的活跃状态,而其他的成员会处于 inactive 的非活跃状态(也就是未定义状态)。这就导致了我们对这些未定义数据成员的访问都算是未定义行为。而大多数的编译器并不会阻止我们对这些非活跃成员的访问。

std::variant 是一个对 union 的封装,它不允许用户访问那些可能引起未定义行为的非活跃成员。它提供 std::get() 的接口给用户来访问某个类型或索引的值。当用户访问到非活跃的成员时,就会抛出一个异常。我们用下面这个例子说明一下:

#include <iostream>
#include <variant>

int main() {
    std::variant<short, int, float, double> U;
    
    U = 7; // assigns 7, which is interpreted as int
    std::cout << std::get<short>(U) << std::endl; // not okay, U currently holds int, throw std::bad_variant_access type exception
    std::cout << std::get<int>(U) << std::endl; // okay
    
	U = 7.5; // assigns 7.5, which is interpreted as double
	std::cout << std::get<double>(U) << std::endl; // okay
	std::cout << std::get<float>(U) << std::endl; // not okay
    return 0;
}

此外,我们还可以通过 std::get_if() 来获取 std::variant 中存储的值(指针)。和上面的 std::get() 不同的是, std::get_if() 并不会抛出异常,如果类型不匹配或者提供的索引是无效的,它会返回一个 nullptr。如果类型是匹配的,就会返回指向存储值的指针。

std::variant<short, int> U = 42;
if (int* pval = std::get_if<int>(&U)) {
    std::cout << "Value: " << *pval << std::endl;
} else {
    std::cout << "Type mismatch or invalid access." << std::endl;
}

Member Index

为了实现类型安全, std::variant 额外用一个 4 字节的 index 值来存储当前处于活跃状态成员变量的索引值。这也就是为什么当你用 sizeof() 查看大小时,你会发现 std::variant 类型往往比相同结构的 union 大四个字节。

std::variant 实际上就相当于:

namespace std {
    template<typename... Types>
    class variant {
    private:
        union Storage {
            std::aligned_union_t<0, Types...> data; // aligned storage for the largest type
        } storage;
        int type_index; // keeps track of the current active type
    public:
        // Constructors, destructors, assignment operators, and member functions would be implemented here
        // Example accessor function
        int index() const {
            return type_index;
        }
    };
}

每个值都能够对应着一个 union 里面的类型,通过当前保存的 index 值,std::variant 就能够知道哪个数据成员是活跃的,从而保证了类型安全。我们用 index() 函数来获取当前活跃类型的索引值。

#include <iostream>
#include <variant>

int main() {
    std::variant<short, int, float, double> U;
    
    U = static_cast<short>(7); // assigns to short type, which index is 0
    std::cout << U.index() << std::endl; // 0
    
    U = static_cast<int>(7); //  assigns to short type, which index is 0 1
    std::cout << U.index() << std::endl; // 1
    
	U = static_cast<float>(7.5); //  assigns to float type, which index is 2
    std::cout << U.index() << std::endl; // 2
    
	U = static_cast<double>(7.5); //  assigns to double type, which index is 3
    std::cout << U.index() << std::endl; // 3
    
    return 0;
}

既然索引对应着唯一的类型,我们实际上也可以直接用 index 值对数据成员进行访问。同样的,如果访问的索引并不是活跃成员的索引,也会抛出一个异常。如:

#include <iostream>
#include <variant>

int main() {
    std::variant<short, int, float, double> U = 7; // int type, index = 1
    std::cout << std::get<1>(U) << std::endl; // 7
    std::cout << std::get<0>(U) << std::endl; // throw std::bad_variant_access
    return 0;
}

Exception Control

当抛出异常时,我们可以对其进行捕获并处理。

#include <iostream>
#include <variant>

int main(){
	std::variant<short, int> U = 7; // int type, index = 1
    try {
        std::cout << std::get<short>(U) << std::endl;
	} catch (const std::bad_variant_access& ex) {
        std::cout << ex.what() << std::endl;
    }
    return 0;
}