一、variant是什么 std::variant是C++17标准库引入的一种类型安全的联合体(union)实现,它位于头文件<variant>中。variant可以存储其模板参数列表中任意一种类型的值,类似于C语言中的union,但具有以下关键区别:variant会跟踪当前存储的值的类型,并且在使用时需要进行类型检查,这使得variant成为类型安全的异构数据容器。
传统的C语言union虽然可以存储不同类型的数据,但它存在严重的类型安全问题。程序员必须手动跟踪当前union中存储的是哪种类型,并在访问时进行正确的类型转换。如果类型不匹配或忘记进行类型检查,就会导致未定义行为。此外,union不能包含非平凡类型(non-trivial types),如包含析构函数的类型,这大大限制了它的使用范围。
std::variant解决了这些问题。variant可以包含任何可拷贝构造的类型,包括具有析构函数的类型。当variant的值被替换或variant本身被销毁时,会自动调用相应类型的析构函数。variant还提供了std::visit函数和std::variant_alternative等辅助工具,使得类型安全的访问成为可能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <variant> #include <string> #include <iostream> using MyVariant = std ::variant<int , double , std ::string >;void basicExample () { MyVariant v1 = 42 ; MyVariant v2 = 3.14 ; MyVariant v3 = "hello" ; std ::cout << std ::get<int >(v1) << std ::endl ; std ::cout << std ::get<double >(v2) << std ::endl ; std ::cout << std ::get<std ::string >(v3) << std ::endl ; }
variant的核心设计理念是提供一种”类型安全的联合体”。每个variant实例在任何时候都恰好持有其类型列表中的一种类型的值(或者处于空状态,如果使用std::monostate作为第一个类型)。variant的大小是其最大类型的对齐要求加上一点开销,用于存储当前持有类型的索引。
variant使用std::visit进行访问,这要求所有被访问的类型都能处理相同的操作。这种设计模式被称为”双分派”(double dispatch),它允许我们根据variant中存储的实际类型来执行相应的操作。与传统的类型转换和switch语句相比,这种方式更加类型安全,也更容易维护。
二、variant有什么作用 std::variant在现代C++编程中扮演着重要的角色,它提供了一种优雅的解决方案来处理需要存储多种可能类型的数据场景。理解variant的作用对于编写健壮、可维护的C++代码至关重要。
第一个核心作用是实现类型安全的异构存储。与传统的union相比,variant在编译期和运行时都提供了类型安全保障。在编译期,variant会确保只有有效的类型才能被存储;在运行时,通过std::visit或std::get_if可以安全地访问variant中的值,如果类型不匹配会抛出异常或返回空指针,而不会导致未定义行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <variant> #include <iostream> void typeSafetyDemo () { std ::variant<int , double , std ::string > v = 100 ; std ::cout << "Value: " << std ::get<int >(v) << std ::endl ; try { std ::cout << std ::get<double >(v) << std ::endl ; } catch (const std ::bad_variant_access& e) { std ::cout << "Exception: " << e.what() << std ::endl ; } }
第二个核心作用是消除原始指针和void*的使用。在C++11之前,实现类似功能通常需要使用void*配合类型信息存储,或者使用指向基类的指针和虚函数实现多态。这些方法都有各自的缺点:void*不安全且丢失了类型信息;虚函数需要预先定义类层次结构,不够灵活。variant提供了一种类型安全、零运行时开销的替代方案。
1 2 3 4 5 6 7 8 9 struct Message { std ::variant<int , std ::string , double > payload; int typeTag; }; using MessageV2 = std ::variant<int , std ::string , double >;
第三个核心作用是简化状态机的实现。状态机中的每个状态可能有不同的数据需要维护,使用variant可以自然地表达这种”每个状态一种数据结构”的需求。variant的std::visit机制非常适合实现状态转换逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 enum class State { Idle, Processing, Completed, Error };struct ProcessingData { int progress; }; struct ErrorData { std ::string message; }; using StateData = std ::monostate; using StateData = std ::variant< std ::monostate, ProcessingData, std ::monostate, ErrorData >;
第四个核心作用是实现编译期多态。与运行时的虚函数多态不同,variant与std::visit结合可以实现编译期的分派,这通常带来更好的性能,因为虚函数调用的开销在编译期就被消除了。此外,编译期多态不需要类继承层次结构,使得代码更加扁平化和模块化。
第五个核心作用是作为std::expected等现代C++模式的基础。C++23引入的std::expected以及一些库中实现的Result类型,都可以使用variant作为其内部实现。这证明了variant作为一种基础构建块的通用性和重要性。
三、variant怎么使用 variant的使用涉及多个方面,包括基本操作、访问方式、值修改、状态查询等。下面将详细介绍这些使用技巧,这些内容是本文的重点部分。
3.1 variant的基本操作 创建variant非常简单,只需要指定它可以存储的类型列表,然后通过构造函数或赋值操作来初始化它。variant会自动使用列表中的第一个类型进行默认初始化(如果该类型支持默认构造),或者你可以通过其他类型进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <variant> #include <string> #include <iostream> void basicOperations () { std ::variant<int , double , std ::string > v1; std ::variant<int , double , std ::string > v2 = 42 ; std ::variant<int , double , std ::string > v3 = 3.14 ; std ::variant<int , double , std ::string > v4 = std ::string ("hello" ); std ::variant<int , double , std ::string > v5; v5.emplace<std ::string >("world" ); std ::variant<int , double , std ::string > v6{std ::in_place_type<double >, 2.718 }; std ::variant<int , double , std ::string > v7{std ::in_place_index<2 >, "variant" }; }
variant的赋值操作遵循”析构旧值,构造新值”的语义。如果新值的类型与当前存储的类型相同,则会调用该类型的拷贝赋值或移动赋值运算符;如果类型不同,则会先析构当前值,然后构造新值。
1 2 3 4 5 6 7 8 9 10 11 12 void assignmentDemo () { std ::variant<int , std ::string > v = 10 ; v = 20 ; v = std ::string ("hello" ); v.emplace<int >(100 ); }
3.2 访问variant中的值 访问variant中的值有多种方式,每种方式都有其适用场景。主要的访问方式包括std::get、std::get_if以及std::visit。
std::get是最直接的访问方式,它要求调用者明确知道要获取哪种类型。如果variant当前存储的不是请求的类型,会抛出std::bad_variant_access异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <variant> #include <iostream> #include <string> void getDemo () { std ::variant<int , double , std ::string > v = "test" ; std ::cout << std ::get<std ::string >(v) << std ::endl ; std ::cout << std ::get<2 >(v) << std ::endl ; try { std ::cout << std ::get<int >(v) << std ::endl ; } catch (const std ::bad_variant_access& e) { std ::cerr << "Type mismatch: " << e.what() << std ::endl ; } }
std::get_if返回指向值的指针(如果类型匹配)或空指针(如果类型不匹配),这种方式更安全,适合在不确定variant当前类型的情况下进行访问。
1 2 3 4 5 6 7 8 9 10 11 12 void getIfDemo () { std ::variant<int , double , std ::string > v = 42 ; if (auto * intPtr = std ::get_if<int >(&v)) { std ::cout << "Got int: " << *intPtr << std ::endl ; } else if (auto * doublePtr = std ::get_if<double >(&v)) { std ::cout << "Got double: " << *doublePtr << std ::endl ; } else if (auto * stringPtr = std ::get_if<std ::string >(&v)) { std ::cout << "Got string: " << *stringPtr << std ::endl ; } }
std::visit是访问variant最强大的方式,它允许我们对variant中存储的值应用一个可调用对象(函数、lambda等)。visitor必须能够处理variant中的所有可能类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <variant> #include <iostream> #include <string> #include <functional> void visitDemo () { std ::variant<int , double , std ::string > v = "hello" ; std ::visit([](auto && arg) { std ::cout << "Value: " << arg << std ::endl ; }, v); std ::variant<int , double > v1 = 10 ; std ::variant<int , double > v2 = 20 ; std ::visit([](auto a, auto b) { std ::cout << "Sum: " << a + b << std ::endl ; }, v1, v2); }
3.3 在visitor中使用类型信息 有的时候,visitor需要根据当前值的具体类型来执行不同的操作。C++17提供了多种方式来获取这些类型信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <variant> #include <iostream> #include <type_traits> void typeBasedVisitor () { std ::variant<int , double , std ::string > v; std ::visit([](auto && arg) { using T = std ::decay_t <decltype (arg)>; if constexpr (std ::is_same_v<T, int >) { std ::cout << "Processing int: " << arg << std ::endl ; } else if constexpr (std ::is_same_v<T, double >) { std ::cout << "Processing double: " << arg << std ::endl ; } else if constexpr (std ::is_same_v<T, std ::string >) { std ::cout << "Processing string: " << arg << std ::endl ; } }, v); }
std::visit返回visitor的返回值,这使得我们可以根据variant中的值计算结果:
1 2 3 auto result = std ::visit([](auto && arg) -> double { return static_cast <double >(arg); }, v);
3.4 variant的状态查询 variant提供了多个成员函数来查询其当前状态,这些函数对于调试和日志记录非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <variant> #include <iostream> void stateQueries () { std ::variant<int , double , std ::string > v = 42 ; std ::cout << "Index: " << v.index() << std ::endl ; std ::cout << "holds_alternative<int>: " << std ::holds_alternative<int >(v) << std ::endl ; std ::cout << "holds_alternative<double>: " << std ::holds_alternative<double >(v) << std ::endl ; }
3.5 处理异常情况 当variant的赋值操作或其他修改操作抛出异常时,variant会进入一种特殊的状态。理解这种行为对于编写健壮的代码很重要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <variant> #include <iostream> #include <stdexcept> struct ThrowingType { int value; ThrowingType(int v) : value(v) {} ThrowingType(const ThrowingType&) { throw std ::runtime_error("copy error" ); } ThrowingType(ThrowingType&&) noexcept { throw std ::runtime_error("move error" ); } }; void exceptionHandling () { std ::variant<int , ThrowingType> v = 10 ; try { v = ThrowingType(20 ); } catch (const std ::exception& e) { if (v.valueless_by_exception) { std ::cout << "Variant is now valueless" << std ::endl ; } } }
3.6 使用std::monostate处理”空”状态 有些情况下,我们可能需要一个”空”或”无效”状态来表示variant当前没有有效的值。C++17使用std::monostate类型来实现这个目的。std::monostate是一个空的、结构体类型,它本身不携带任何有意义的数据,只是作为一个占位符存在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <variant> #include <iostream> void monostateDemo () { using OptionalInt = std ::variant<std ::monostate, int , double >; OptionalInt v1; OptionalInt v2 = std ::monostate{}; if (std ::holds_alternative<std ::monostate>(v1)) { std ::cout << "v1 is empty" << std ::endl ; } std ::visit([](auto && arg) { using T = std ::decay_t <decltype (arg)>; if constexpr (std ::is_same_v<T, std ::monostate>) { std ::cout << "Empty value" << std ::endl ; } else { std ::cout << "Value: " << arg << std ::endl ; } }, v1); }
四、variant有哪些使用场景 variant在实际编程中有广泛的应用场景,从简单的值替换到复杂的状态机实现,variant都提供了一种类型安全、表达力强的解决方案。理解这些使用场景有助于在实际项目中做出正确的设计决策。
第一个常见场景是实现可选值(Optional Values)。在C++17之前,我们通常使用指针或特殊值(如nullptr、-1)来表示可选值。使用std::monostate作为第一个类型的variant可以更清晰地表达”可能没有值”的语义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 template <typename T>using Optional = std ::variant<std ::monostate, T>;Optional<std ::string > findName (int id) { if (id > 0 && id <= 100 ) { return std ::string ("Name" ) + std ::to_string(id); } return std ::monostate{}; } void useOptional () { auto result = findName(50 ); std ::visit([](auto && arg) { if constexpr (std ::is_same_v<std ::decay_t <decltype (arg)>, std ::monostate>) { std ::cout << "Not found" << std ::endl ; } else { std ::cout << "Found: " << arg << std ::endl ; } }, result); }
第二个常见场景是实现解析器和数据转换。在解析JSON、XML或其他格式的数据时,解析结果可能有多种类型(数字、字符串、布尔值、嵌套对象等),variant是表示这种异构数据的理想选择。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <variant> #include <string> #include <vector> #include <unordered_map> using JsonValue = std ::variant< std ::nullptr_t , bool , double , std ::string , std ::vector <JsonValue>, std ::unordered_map <std ::string , JsonValue> >; JsonValue buildJson () { std ::unordered_map <std ::string , JsonValue> object; object["name" ] = std ::string ("Alice" ); object["age" ] = 30 ; object["active" ] = true ; std ::vector <JsonValue> skills = { std ::string ("C++" ), std ::string ("Python" ) }; object["skills" ] = skills; return object; }
第三个常见场景是实现命令模式(Command Pattern)。不同的命令可能有不同的参数类型,使用variant可以统一表示不同类型的命令对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <variant> #include <string> #include <iostream> struct CreateUser { std ::string name; std ::string email; }; struct DeleteUser { int userId; }; struct UpdateUser { int userId; std ::string field; std ::string newValue; }; using Command = std ::variant<CreateUser, DeleteUser, UpdateUser>;void executeCommand (const Command& cmd) { std ::visit([](auto && c) { using T = std ::decay_t <decltype (c)>; if constexpr (std ::is_same_v<T, CreateUser>) { std ::cout << "Creating user: " << c.name << std ::endl ; } else if constexpr (std ::is_same_v<T, DeleteUser>) { std ::cout << "Deleting user: " << c.userId << std ::endl ; } else if constexpr (std ::is_same_v<T, UpdateUser>) { std ::cout << "Updating user " << c.userId << ": " << c.field << " = " << c.newValue << std ::endl ; } }, cmd); }
第四个常见场景是实现状态机。状态机中的每种状态通常有不同的关联数据,使用variant可以自然地表达这种关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <variant> #include <string> #include <iostream> struct IdleState { }; struct ConnectingState { std ::string server; int port; }; struct ConnectedState { int connectionId; double uptime; }; struct ErrorState { std ::string message; int errorCode; }; using NetworkState = std ::variant< IdleState, ConnectingState, ConnectedState, ErrorState >; class NetworkConnection { NetworkState state_; public : void connect (const std ::string & server, int port) { state_ = ConnectingState{server, port}; } void disconnect () { state_ = IdleState{}; } void process () { std ::visit([](auto && s) { using T = std ::decay_t <decltype (s)>; if constexpr (std ::is_same_v<T, IdleState>) { std ::cout << "Waiting for connection..." << std ::endl ; } else if constexpr (std ::is_same_v<T, ConnectingState>) { std ::cout << "Connecting to " << s.server << ":" << s.port << std ::endl ; } else if constexpr (std ::is_same_v<T, ConnectedState>) { std ::cout << "Connected (ID: " << s.connectionId << ")" << std ::endl ; } else if constexpr (std ::is_same_v<T, ErrorState>) { std ::cout << "Error: " << s.message << " (code: " << s.errorCode << ")" << std ::endl ; } }, state_); } };
第五个常见场景是实现事件处理系统。不同类型的事件可能有不同的负载数据,使用variant可以统一处理所有类型的事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <variant> #include <string> #include <chrono> struct MouseClick { int x, y; int button; }; struct KeyPress { char key; bool shift; }; struct WindowResize { int width, height; }; struct TimerElapsed { std ::chrono::steady_clock::time_point timestamp; }; using Event = std ::variant<MouseClick, KeyPress, WindowResize, TimerElapsed>;class EventHandler {public : void handleEvent (const Event& event) { std ::visit([this ](auto && e) { processEvent(e); }, event); } private : void processEvent (const MouseClick& click) { std ::cout << "Mouse click at (" << click.x << ", " << click.y << ") button: " << click.button << std ::endl ; } void processEvent (const KeyPress& key) { std ::cout << "Key pressed: '" << key.key << "' (shift: " << key.shift << ")" << std ::endl ; } void processEvent (const WindowResize& resize) { std ::cout << "Window resized to " << resize.width << "x" << resize.height << std ::endl ; } void processEvent (const TimerElapsed& timer) { std ::cout << "Timer elapsed at timestamp" << std ::endl ; } };
第六个常见场景是实现函数的多返回类型。某些函数可能有不同类型的返回结果,使用variant可以优雅地处理这种情况,而不需要使用输出参数或异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <variant> #include <string> #include <iostream> using CalcResult = std ::variant<double , std ::string >;CalcResult divide (double a, double b) { if (b == 0 ) { return std ::string ("Error: Division by zero" ); } return a / b; } void printResult (const CalcResult& result) { std ::visit([](auto && arg) { if constexpr (std ::is_same_v<std ::decay_t <decltype (arg)>, std ::string >) { std ::cerr << arg << std ::endl ; } else { std ::cout << "Result: " << arg << std ::endl ; } }, result); }
五、variant与tuple的异同点 std::variant和std::tuple都是C++标准库中的异构容器,但它们有本质的区别。理解这些区别对于在实际编程中选择正确的工具至关重要。
5.1 相同点 首先,让我们看看variant和tuple的相同点:
两者都是异构容器,可以存储不同类型的数据。tuple可以存储任意数量的不同类型,variant可以存储任意一种类型(从给定的类型列表中选择)。
两者都支持编译期类型操作。C++标准库为两者都提供了类型访问的工具:std::tuple_element和std::variant_alternative分别用于获取元素/候选类型。
两者都支持std::visit进行访问。对于tuple,std::visit会访问所有元素;对于variant,std::visit会根据当前存储的类型只访问一个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <variant> #include <tuple> #include <iostream> void commonFeatures () { std ::tuple<int , double , std ::string > t{1 , 2.0 , "three" }; std ::variant<int , double , std ::string > v{1 }; std ::visit([](auto && arg) { std ::cout << "Visit result: " << arg << std ::endl ; }, v); std ::visit([](auto &&... args) { ((std ::cout << args << " " ), ...); std ::cout << std ::endl ; }, t); }
5.2 不同点 虽然variant和tuple都是异构容器,但它们在语义和使用场景上有根本的区别:
存储语义不同 :tuple同时持有所有类型的值(以成员变量的形式),而variant只持有一种类型的值。这是两者最根本的区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <variant> #include <tuple> #include <iostream> void storageDifference () { std ::tuple<int , double , std ::string > t{1 , 2.0 , "hello" }; std ::cout << "Tuple size (incomplete): " << sizeof (t) << " bytes" << std ::endl ; std ::variant<int , double , std ::string > v{1 }; std ::cout << "Variant size: " << sizeof (v) << " bytes" << std ::endl ; }
访问方式不同 :tuple的访问是位置固定的(通过std::get<N>),而variant的访问是类型或索引的(通过std::get<T>或std::get<N>),并且类型必须匹配当前存储的类型。
1 2 3 4 5 6 7 8 9 10 11 12 void accessDifference () { std ::tuple<int , std ::string > t{42 , "hello" }; std ::variant<int , std ::string > v{42 }; std ::cout << std ::get<0 >(t) << std ::endl ; std ::cout << std ::get<1 >(t) << std ::endl ; std ::cout << std ::get<int >(v) << std ::endl ; }
大小和内存布局不同 :tuple的大小是其所有元素大小的总和(加上可能的填充),而variant的大小是其最大元素的大小加上一些开销(用于存储类型索引)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <tuple> #include <variant> #include <iostream> struct LargeStruct { char data[1000 ]; }; void sizeComparison () { std ::tuple<int , LargeStruct> t; std ::variant<int , LargeStruct> v; std ::cout << "Tuple size: " << sizeof (t) << " bytes" << std ::endl ; std ::cout << "Variant size: " << sizeof (v) << " bytes" << std ::endl ; }
默认构造行为不同 :tuple在默认构造时会默认构造所有元素,而variant只默认构造其第一个类型(如果第一个类型可默认构造的话)。
1 2 3 4 5 6 void defaultConstruction () { std ::tuple<int , std ::string > t; std ::variant<int , std ::string > v; }
适用场景不同 :tuple适用于”一组相关的值”,如函数的多个返回值、数据的聚合作”组合”;variant适用于”一个值,可能是多种类型之一”,如解析结果、状态数据。
5.3 选择指南 在选择variant还是tuple时,考虑以下问题:
如果需要同时存储多个不同类型的值,选择tuple。
如果需要从多个类型中选择一个存储,选择variant。
如果需要固定大小的数据集合,选择tuple。
如果需要运行时决定使用哪种类型,选择variant。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 std::tuple<int, double, std::string> getStats() { return {100 , 98.5 , "Alice" }; } std::variant<int, std::string, double> processInput(const std::string& input) { if (input.empty()) { return std ::string ("Error: empty input" ); } if (input == "yes" ) { return 1 ; } return 3.14 ; }
六、如何避免滥用variant 虽然std::variant是一个强大的工具,但不当使用会导致代码复杂度增加、维护困难甚至运行时错误。理解常见的滥用模式以及如何避免它们,对于编写高质量的C++代码至关重要。
第一个常见的滥用是过度使用variant作为”万能类型”。variant应该用于真正需要表示”多种可能类型”的情况,而不是为了避免设计决策。如果一个variant包含太多不相关的类型,可能意味着应该重新审视设计。
1 2 3 4 5 6 7 8 9 10 11 using BadDesign = std ::variant< int , double , std ::string , std ::vector <int >, std ::map <std ::string , int >, CustomClass, std ::function<void ()>, std ::thread >; using Value = std ::variant<int , double , std ::string >; using Action = std ::variant<std ::function<void ()>, std ::thread>;
第二个常见的滥用是在不需要运行时类型区分的场景下使用variant。如果所有可能的类型都需要相同的处理逻辑,使用variant可能过于复杂。在这种情况下,模板可能是更好的选择。
1 2 3 4 5 6 7 VariantProcesser v = 42 ; std ::visit([](auto && x) { process(x); }, v); template <typename T>void process (T&& value) { }
第三个常见的滥用是忽略variant的异常安全性。当variant的赋值操作抛出异常时,variant可能进入valueless_by_exception状态。忽略这种状态会导致后续访问出现问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <variant> #include <iostream> #include <stdexcept> struct MayThrow { int value; MayThrow(int v) : value(v) {} MayThrow(const MayThrow& other) : value(other.value) { if (value < 0 ) throw std ::runtime_error("Negative value" ); } }; void safeVariantUsage () { std ::variant<int , MayThrow> v = 10 ; try { v = MayThrow(20 ); } catch (...) { if (v.valueless_by_exception) { std ::cerr << "Variant is valueless due to exception" << std ::endl ; return ; } } }
第四个常见的滥用是频繁改变variant的类型。variant的类型改变涉及析构和构造操作,频繁的类型变化会带来性能开销。如果需要频繁改变类型,可能需要考虑其他设计。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void performanceConcern () { std ::variant<int , std ::string , double > v; for (int i = 0 ; i < 1000000 ; ++i) { if (i % 3 == 0 ) { v = i; } else if (i % 3 == 1 ) { v = std ::to_string(i); } else { v = static_cast <double >(i) / 1000.0 ; } } }
第五个常见的滥用是忘记处理所有可能的类型。在编写visitor时,如果遗漏了某些类型的处理,编译器会报错(这是好的),但如果使用std::get或std::get_if,遗漏的情况可能导致运行时错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void completeHandling () { std ::variant<int , double , std ::string > v; std ::visit([](auto && arg) { using T = std ::decay_t <decltype (arg)>; if constexpr (std ::is_same_v<T, int >) { } }, v); if (std ::get_if<int >(&v)) { } else if (std ::get_if<double >(&v)) { } else if (std ::get_if<std ::string >(&v)) { } }
第六个常见的滥用是将variant与原始类型转换混用。虽然variant提供了类型转换的便利,但过度依赖隐式转换可能导致意外行为。
1 2 3 4 5 6 7 8 9 10 11 void implicitConversion () { std ::variant<int , double > v = 42 ; double d = std ::get<double >(v); if (auto * pInt = std ::get_if<int >(&v)) { double d = static_cast <double >(*pInt); } }
为了避免滥用variant,遵循以下原则:
保持variant类型列表的简洁和内聚。每个variant应该代表一个清晰的概念。
在编写visitor时,使用if constexpr确保所有类型都被处理。
注意异常安全性,特别是在处理可能抛出异常的赋值操作时。
考虑性能影响,特别是在高频调用的代码路径中。
将variant作为明确的设计选择,而不是避免设计决策的权宜之计。
七、相关标准库 C++17标准库为std::variant提供了丰富的配套设施,包括类型特性、辅助函数和算法。了解这些相关组件对于充分发挥variant的潜力至关重要。
7.1 类型特性 标准库提供了多个与variant相关的类型特性,用于在编译期查询variant的类型信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <variant> #include <type_traits> void typeTraitsDemo () { using MyVariant = std ::variant<int , double , std ::string >; std ::cout << "variant_size: " << std ::variant_size<MyVariant>::value << std ::endl ; using FirstType = std ::variant_alternative<0 , MyVariant>::type; using SecondType = std ::variant_alternative<1 , MyVariant>::type; std ::cout << "aligned_storage: " << alignof (MyVariant) << std ::endl ; }
7.2 辅助函数 标准库提供了多个辅助函数来简化variant的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <variant> #include <iostream> void helperFunctions () { using MyVariant = std ::variant<int , double , std ::string >; MyVariant v = 42 ; std ::cout << "index: " << v.index() << std ::endl ; std ::cout << "holds_alternative<int>: " << std ::holds_alternative<int >(v) << std ::endl ; int value = std ::get<int >(v); auto * ptr = std ::get_if<int >(&v); }
7.3 std::visit详解 std::visit是使用variant最强大的方式,它允许我们对variant中存储的值应用一个visitor。std::visit可以接受一个或多个variant参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <variant> #include <iostream> #include <string> void visitAdvanced () { std ::variant<int , double > v1 = 10 ; std ::variant<int , double > v2 = 20 ; std ::visit([](auto a, auto b) { std ::cout << "a + b = " << (a + b) << std ::endl ; }, v1, v2); int sum = 0 ; std ::visit([&sum](auto value) { sum += value; }, v1); std ::cout << "Sum: " << sum << std ::endl ; auto result = std ::visit([](auto a, auto b) -> double { return a + b; }, v1, v2); std ::cout << "Result: " << result << std ::endl ; }
7.4 std::monostate std::monostate是一个辅助类型,用于表示variant的”空”或”无效”状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <variant> #include <iostream> void monostateUsage () { using OptionalInt = std ::variant<std ::monostate, int >; OptionalInt empty; OptionalInt hasValue = 42 ; if (std ::holds_alternative<std ::monostate>(empty)) { std ::cout << "empty has no value" << std ::endl ; } std ::visit([](auto && arg) { if constexpr (std ::is_same_v<std ::decay_t <decltype (arg)>, std ::monostate>) { std ::cout << "Empty" << std ::endl ; } else { std ::cout << "Value: " << arg << std ::endl ; } }, empty); }
7.5 异常类 当variant访问失败时,会抛出std::bad_variant_access异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <variant> #include <iostream> #include <stdexcept> void exceptionHandling () { std ::variant<int , double > v = 10 ; try { std ::cout << std ::get<double >(v) << std ::endl ; } catch (const std ::bad_variant_access& e) { std ::cout << "Variant access error: " << e.what() << std ::endl ; } }
7.6 组合使用 在实际应用中,经常需要将variant与其他标准库组件组合使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <variant> #include <vector> #include <algorithm> void combineWithContainers () { std ::vector <std ::variant<int , std ::string , double >> values; values.push_back(1 ); values.push_back("hello" ); values.push_back(3.14 ); std ::for_each(values.begin(), values.end(), [](const auto & v) { std ::visit([](auto && arg) { std ::cout << arg << " " ; }, v); }); std ::cout << std ::endl ; } #include <functional> using Operation = std ::variant< std ::function<int (int , int )>, std ::function<std ::string (const std ::string &)> >; void functionVariant () { Operation op = [](int a, int b) { return a + b; }; std ::visit([](auto && func) { if constexpr (std ::is_invocable_v<decltype (func), int , int >) { std ::cout << "Result: " << func(10 , 20 ) << std ::endl ; } }, op); }
八、可以实现的设计模式 std::variant可以用于实现多种经典的设计模式,它的类型安全性和灵活性使其成为替代传统面向对象实现的有力工具。下面介绍几种与variant密切相关的设计模式及其实现方式。
8.1 状态模式 使用variant实现状态模式是一种高效且类型安全的方法。与传统的使用虚函数的实现相比,variant-based状态模式避免了虚函数调用的开销,并且状态转换的逻辑更加清晰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <variant> #include <iostream> #include <string> class TCPConnection ;struct TCPState { virtual ~TCPState() = default ; virtual void process (TCPConnection& conn) = 0 ; }; struct ClosedState : TCPState { void process (TCPConnection& conn) override ; }; struct EstablishedState : TCPState { void process (TCPConnection& conn) override ; }; struct ListenState : TCPState { void process (TCPConnection& conn) override ; }; struct Closed { void enter (TCPConnection& conn) { std ::cout << "Entering Closed state" << std ::endl ; } void exit (TCPConnection& conn) { std ::cout << "Exiting Closed state" << std ::endl ; } }; struct Established { void enter (TCPConnection& conn) { std ::cout << "Connection established" << std ::endl ; } void exit (TCPConnection& conn) { std ::cout << "Closing connection" << std ::endl ; } }; struct Listening { int backlog = 10 ; void enter (TCPConnection& conn) { std ::cout << "Listening on port" << std ::endl ; } void exit (TCPConnection& conn) { std ::cout << "Stopped listening" << std ::endl ; } }; class TCPConnection {public : using State = std ::variant<Closed, Established, Listening>; State currentState; void transitionTo (State newState) { std ::visit([this ](auto && s) { s.exit (*this ); }, currentState); currentState = std ::move(newState); std ::visit([this ](auto && s) { s.enter(*this ); }, currentState); } void processData (const std ::string & data) { std ::visit([&data](auto && s) { using T = std ::decay_t <decltype (s)>; if constexpr (std ::is_same_v<T, Established>) { std ::cout << "Processing data: " << data << std ::endl ; } else { std ::cout << "Cannot process data in this state" << std ::endl ; } }, currentState); } };
8.2 访问者模式 variant为访问者模式提供了一种类型安全的替代实现。相比传统的双分派实现,variant-based访问者模式更简洁,且编译器会确保所有类型都被处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <variant> #include <iostream> #include <string> struct NumberNode { int value; }; struct BinaryOpNode { char op; void * left; void * right; }; struct StringNode { std ::string value; }; using ASTNode = std ::variant<NumberNode, BinaryOpNode, StringNode>;struct PrintVisitor { void operator () (const NumberNode& n) { std ::cout << n.value; } void operator () (const BinaryOpNode& op) { std ::cout << "(" ; std ::visit(*this , *static_cast <ASTNode*>(op.left)); std ::cout << " " << op.op << " " ; std ::visit(*this , *static_cast <ASTNode*>(op.right)); std ::cout << ")" ; } void operator () (const StringNode& s) { std ::cout << "\"" << s.value << "\"" ; } }; struct EvalVisitor { int operator () (const NumberNode& n) { return n.value; } int operator () (const BinaryOpNode& op) { int left = std ::visit(*this , *static_cast <ASTNode*>(op.left)); int right = std ::visit(*this , *static_cast <ASTNode*>(op.right)); switch (op.op) { case '+' : return left + right; case '-' : return left - right; case '*' : return left * right; case '/' : return left / right; default : return 0 ; } } int operator () (const StringNode&) { return 0 ; } };
8.3 策略模式 使用variant实现策略模式允许在编译期选择不同的策略,同时保持运行时绑定的灵活性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <variant> #include <iostream> #include <functional> struct NoCompression { std ::string compress (const std ::string & data) { return data; } std ::string decompress (const std ::string & data) { return data; } }; struct GzipCompression { std ::string compress (const std ::string & data) { return "[gzip]" + data; } std ::string decompress (const std ::string & data) { return data.substr(6 ); } }; struct DeflateCompression { std ::string compress (const std ::string & data) { return "[deflate]" + data; } std ::string decompress (const std ::string & data) { return data.substr(9 ); } }; class Compressor {public : using CompressionStrategy = std ::variant<NoCompression, GzipCompression, DeflateCompression>; CompressionStrategy strategy; Compressor(CompressionStrategy s) : strategy(std ::move(s)) {} std ::string compress (const std ::string & data) { return std ::visit([&data](auto && s) { return s.compress(data); }, strategy); } std ::string decompress (const std ::string & data) { return std ::visit([&data](auto && s) { return s.decompress(data); }, strategy); } };
8.4 命令模式 variant可以用于实现命令模式,统一处理不同类型的命令对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <variant> #include <iostream> #include <string> #include <memory> struct CreateCommand { std ::string name; void execute () const { std ::cout << "Creating: " << name << std ::endl ; } }; struct DeleteCommand { int id; void execute () const { std ::cout << "Deleting ID: " << id << std ::endl ; } }; struct UpdateCommand { int id; std ::string field; std ::string value; void execute () const { std ::cout << "Updating ID " << id << ": " << field << " = " << value << std ::endl ; } }; class CommandQueue {public : using CommandType = std ::variant<CreateCommand, DeleteCommand, UpdateCommand>; std ::vector <CommandType> commands; void addCommand (CommandType cmd) { commands.push_back(std ::move(cmd)); } void executeAll () { for (const auto & cmd : commands) { std ::visit([](const auto & c) { c.execute(); }, cmd); } } };
8.5 解析器模式 variant非常适合实现解析器,因为解析结果可能有多种类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <variant> #include <iostream> #include <string> #include <vector> struct NumberResult { double value; };struct StringResult { std ::string value; };struct ErrorResult { std ::string message; };struct ListResult { std ::vector <double > values; };using ParseResult = std ::variant<NumberResult, StringResult, ErrorResult, ListResult>;class Parser {public : ParseResult parse (const std ::string & input) { if (input.empty()) { return ErrorResult{"Empty input" }; } if (input[0 ] == '[' ) { return ListResult{{1.0 , 2.0 , 3.0 }}; } if (isdigit (input[0 ]) || input[0 ] == '-' ) { try { return NumberResult{std ::stod(input)}; } catch (...) { return ErrorResult{"Invalid number" }; } } return StringResult{input}; } }; void printResult (const ParseResult& result) { std ::visit([](auto && r) { using T = std ::decay_t <decltype (r)>; if constexpr (std ::is_same_v<T, NumberResult>) { std ::cout << "Number: " << r.value << std ::endl ; } else if constexpr (std ::is_same_v<T, StringResult>) { std ::cout << "String: " << r.value << std ::endl ; } else if constexpr (std ::is_same_v<T, ErrorResult>) { std ::cout << "Error: " << r.message << std ::endl ; } else if constexpr (std ::is_same_v<T, ListResult>) { std ::cout << "List: " ; for (auto v : r.values) std ::cout << v << " " ; std ::cout << std ::endl ; } }, result); }
8.6 事件系统 使用variant可以实现类型安全的事件系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <variant> #include <iostream> #include <string> #include <vector> #include <functional> struct MouseEvent { int x, y; };struct KeyEvent { char key; bool pressed; };struct ResizeEvent { int width, height; };using Event = std ::variant<MouseEvent, KeyEvent, ResizeEvent>;using EventHandler = std ::function<void (const Event&)>;class EventDispatcher { std ::vector <EventHandler> handlers; public : void subscribe (EventHandler handler) { handlers.push_back(std ::move(handler)); } void dispatch (const Event& event) { for (auto & handler : handlers) { handler(event); } } }; void setupEventSystem () { EventDispatcher dispatcher; dispatcher.subscribe([](const Event& e) { std ::visit([](auto && event) { using T = std ::decay_t <decltype (event)>; if constexpr (std ::is_same_v<T, MouseEvent>) { std ::cout << "Mouse at (" << event.x << ", " << event.y << ")" << std ::endl ; } else if constexpr (std ::is_same_v<T, KeyEvent>) { std ::cout << "Key " << event.key << (event.pressed ? " pressed" : " released" ) << std ::endl ; } else if constexpr (std ::is_same_v<T, ResizeEvent>) { std ::cout << "Resized to " << event.width << "x" << eventendl; } }, e); }); dispatcher.dispatch(MouseEvent{100 , 200 }); dispatcher.dispatch(KeyEvent{'A' , true }); dispatcher.dispatch(ResizeEvent{800 , 600 }); }
通过这些设计模式的variant实现,我们可以看到variant作为一种类型安全的异构联合体,在现代C++编程中具有广泛的应用价值。它不仅简化了代码,还提供了编译期的类型安全保证,是C++17最重要的特性之一。
variant特别适合以下场景:需要表示”一个值,多种可能类型”;需要类型安全的联合体;需要实现状态机或解析器;需要避免使用void*或手动类型标签。使用variant时,应注意保持类型列表的简洁、处理所有可能的类型、考虑异常安全性,并在适当的情况下选择更简单的替代方案(如模板)。