C++17 variant使用介绍

一、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>

// 定义一个可以存储int、double或std::string的variant
using MyVariant = std::variant<int, double, std::string>;

// 简单的variant使用示例
void basicExample() {
MyVariant v1 = 42; // 存储int
MyVariant v2 = 3.14; // 存储double
MyVariant v3 = "hello"; // 存储std::string (通过char数组构造)

std::cout << std::get<int>(v1) << std::endl; // 输出: 42
std::cout << std::get<double>(v2) << std::endl; // 输出: 3.14
std::cout << std::get<std::string>(v3) << std::endl; // 输出: hello
}

variant的核心设计理念是提供一种”类型安全的联合体”。每个variant实例在任何时候都恰好持有其类型列表中的一种类型的值(或者处于空状态,如果使用std::monostate作为第一个类型)。variant的大小是其最大类型的对齐要求加上一点开销,用于存储当前持有类型的索引。

variant使用std::visit进行访问,这要求所有被访问的类型都能处理相同的操作。这种设计模式被称为”双分派”(double dispatch),它允许我们根据variant中存储的实际类型来执行相应的操作。与传统的类型转换和switch语句相比,这种方式更加类型安全,也更容易维护。

二、variant有什么作用

std::variant在现代C++编程中扮演着重要的角色,它提供了一种优雅的解决方案来处理需要存储多种可能类型的数据场景。理解variant的作用对于编写健壮、可维护的C++代码至关重要。

第一个核心作用是实现类型安全的异构存储。与传统的union相比,variant在编译期和运行时都提供了类型安全保障。在编译期,variant会确保只有有效的类型才能被存储;在运行时,通过std::visitstd::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
// 使用variant替代void*和类型标签
struct Message {
std::variant<int, std::string, double> payload;
int typeTag; // 不再需要手动维护类型标签
};

// 使用variant后的改进版本
using MessageV2 = std::variant<int, std::string, double>;
// 类型信息由variant自动维护,无需额外的typeTag

第三个核心作用是简化状态机的实现。状态机中的每个状态可能有不同的数据需要维护,使用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 };

// 使用variant表示每种状态的数据
struct ProcessingData {
int progress;
};

struct ErrorData {
std::string message;
};

using StateData = std::monostate; // Idle状态不需要额外数据
using StateData = std::variant<
std::monostate, // Idle
ProcessingData, // Processing
std::monostate, // Completed (不需要额外数据)
ErrorData // Error
>;

第四个核心作用是实现编译期多态。与运行时的虚函数多态不同,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() {
// 创建variant,使用第一个类型的默认值初始化
std::variant<int, double, std::string> v1; // 值为0

// 使用其他类型的值初始化
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");

// 使用emplace避免不必要的拷贝
std::variant<int, double, std::string> v5;
v5.emplace<std::string>("world"); // 直接构造,避免临时对象

// 使用std::in_place_type指定类型
std::variant<int, double, std::string> v6{std::in_place_type<double>, 2.718};

// 使用std::in_place_index指定索引
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; // 调用int的赋值运算符

// 赋值不同类型
v = std::string("hello"); // 析构int,构造string

// 使用emplace进行原地构造
v.emplace<int>(100); // 析构string,构造int
}

3.2 访问variant中的值

访问variant中的值有多种方式,每种方式都有其适用场景。主要的访问方式包括std::getstd::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";

// 使用lambda作为visitor
std::visit([](auto&& arg) {
std::cout << "Value: " << arg << std::endl;
}, v);

// 多个variant一起访问
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;

// 检查variant当前持有的是哪种类型
std::cout << "Index: " << v.index() << std::endl; // 输出: 0 (int的索引)

// 检查variant是否持有特定类型的值
std::cout << "holds_alternative<int>: " << std::holds_alternative<int>(v) << std::endl; // true
std::cout << "holds_alternative<double>: " << std::holds_alternative<double>(v) << std::endl; // false

// valueless_by_exception: variant是否处于有效状态
// 如果variant中的操作抛出异常,variant可能处于valueless状态
}

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;
}
// variant的状态取决于异常发生的时机
}
}

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() {
// 使用std::monostate作为第一个类型来提供"空"状态
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;
}

// 在visit中处理空状态
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>

// JSON值的简单表示
using JsonValue = std::variant<
std::nullptr_t, // null
bool, // true/false
double, // number
std::string, // string
std::vector<JsonValue>, // array
std::unordered_map<std::string, JsonValue> // object
>;

// 构建JSON对象
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;
};

// 使用variant统一表示所有命令
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;
};

// 使用variant表示所有可能的状态
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::variantstd::tuple都是C++标准库中的异构容器,但它们有本质的区别。理解这些区别对于在实际编程中选择正确的工具至关重要。

5.1 相同点

首先,让我们看看variant和tuple的相同点:

两者都是异构容器,可以存储不同类型的数据。tuple可以存储任意数量的不同类型,variant可以存储任意一种类型(从给定的类型列表中选择)。

两者都支持编译期类型操作。C++标准库为两者都提供了类型访问的工具:std::tuple_elementstd::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
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() {
// tuple同时持有int、double、string三个值
std::tuple<int, double, std::string> t{1, 2.0, "hello"};
std::cout << "Tuple size (incomplete): " << sizeof(t) << " bytes" << std::endl;
// 输出: Tuple包含int、double、string三个对象

// variant只持有一种类型的值
std::variant<int, double, std::string> v{1};
std::cout << "Variant size: " << sizeof(v) << " bytes" << std::endl;
// 输出: Variant只包含一个值加上类型索引
}

访问方式不同: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};

// tuple: 按位置访问
std::cout << std::get<0>(t) << std::endl; // 总是42
std::cout << std::get<1>(t) << std::endl; // 总是"hello"

// variant: 按类型访问(必须与当前存储的类型匹配)
std::cout << std::get<int>(v) << std::endl; // 如果当前是int,正确
// std::get<std::string>(v) 如果当前是int,会抛出异常
}

大小和内存布局不同: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; // ~1004 bytes
std::cout << "Variant size: " << sizeof(v) << " bytes" << std::endl; // ~1008 bytes (加上了索引)
}

默认构造行为不同:tuple在默认构造时会默认构造所有元素,而variant只默认构造其第一个类型(如果第一个类型可默认构造的话)。

1
2
3
4
5
6
void defaultConstruction() {
std::tuple<int, std::string> t; // int=0, string=""

std::variant<int, std::string> v; // int=0
// 如果第一个类型是std::string,variant默认构造string
}

适用场景不同: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
// 使用tuple的场景:函数返回多个值
std::tuple<int, double, std::string> getStats() {
return {100, 98.5, "Alice"};
}

// 使用variant的场景:函数返回可能失败,结果类型不固定
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
// 不好的设计:variant类型过多且不相关
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
// 不必要的使用variant
VariantProcesser v = 42;
std::visit([](auto&& x) { process(x); }, v); // process对所有类型做同样的事

// 更简单的方案:模板函数
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 (...) {
// 必须检查variant是否处于有效状态
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::getstd::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>) {
// 处理int
}
// 如果缺少double和string的处理,编译器会警告或报错
}, v);

// 使用get_if时,需要显式处理所有情况
if (std::get_if<int>(&v)) {
// 处理int
} else if (std::get_if<double>(&v)) {
// 处理double
} else if (std::get_if<std::string>(&v)) {
// 处理string
}
// 如果忘记某个分支,silent bug
}

第六个常见的滥用是将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); // int被提升为double?不,这里会抛出异常

// 正确的做法是明确知道当前类型
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>;

// 获取variant的大小(类型列表中的类型数量)
std::cout << "variant_size: " << std::variant_size<MyVariant>::value << std::endl; // 3

// 获取指定位置的类型
using FirstType = std::variant_alternative<0, MyVariant>::type; // int
using SecondType = std::variant_alternative<1, MyVariant>::type; // double

// 获取variant中最大类型的对齐要求
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;

// 检查variant是否持有特定类型的值
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;

// 使用多个variant调用visit
std::visit([](auto a, auto b) {
std::cout << "a + b = " << (a + b) << std::endl;
}, v1, v2);

// visitor可以有状态
int sum = 0;
std::visit([&sum](auto value) {
sum += value;
}, v1);
std::cout << "Sum: " << sum << std::endl;

// visitor可以返回结果
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() {
// Optional类型的设计模式
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;
}

// 在visit中处理空状态
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() {
// variant的vector
std::vector<std::variant<int, std::string, double>> values;
values.push_back(1);
values.push_back("hello");
values.push_back(3.14);

// 处理vector中的每个variant
std::for_each(values.begin(), values.end(), [](const auto& v) {
std::visit([](auto&& arg) {
std::cout << arg << " ";
}, v);
});
std::cout << std::endl;
}

// 与std::function组合
#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;
};

// 使用variant的状态模式(更轻量级的实现)
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; }
};

// 使用variant的TCPConnection
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;
};

// 使用variant表示所有节点类型
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 << "\"";
}
};

// 使用variant的表达式求值器
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); }
};

// 使用variant的压缩器
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;
}
};

// 使用variant的命令模式
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);
});

// .height << std::发送事件
dispatcher.dispatch(MouseEvent{100, 200});
dispatcher.dispatch(KeyEvent{'A', true});
dispatcher.dispatch(ResizeEvent{800, 600});
}

通过这些设计模式的variant实现,我们可以看到variant作为一种类型安全的异构联合体,在现代C++编程中具有广泛的应用价值。它不仅简化了代码,还提供了编译期的类型安全保证,是C++17最重要的特性之一。

variant特别适合以下场景:需要表示”一个值,多种可能类型”;需要类型安全的联合体;需要实现状态机或解析器;需要避免使用void*或手动类型标签。使用variant时,应注意保持类型列表的简洁、处理所有可能的类型、考虑异常安全性,并在适当的情况下选择更简单的替代方案(如模板)。

感谢您对本站的支持.