跳到主要内容

C++ 17 std::variant

什么是 std::variant?

std::variant 是 C++17 引入的一个新的标准库类型,它允许你在一个变量中存储多种可能的类型中的一个。你可以把它理解为一个类型安全的联合体(union)。与传统的 union 不同,std::variant 知道它当前存储的是哪种类型,并提供了类型安全的访问方式。

备注

std::variant 定义在 <variant> 头文件中,属于 std 命名空间。

为什么需要 std::variant?

在 C++17 之前,如果你想在一个变量中存储多种可能的类型,你可能会使用:

  1. union:但它不能安全地处理有构造函数和析构函数的类型
  2. 基类指针和继承:需要动态内存分配,且类型必须相关
  3. void*:完全丧失了类型安全

std::variant 解决了这些问题,它是类型安全的,支持任意类型组合,不需要动态内存分配。

基本用法

创建和赋值

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

int main() {
// 创建一个可以存储 int 或 std::string 的 variant
std::variant<int, std::string> v;

// 默认情况下,variant 初始化为第一个类型
std::cout << "默认值: " << std::get<int>(v) << std::endl;

// 赋值一个字符串
v = "Hello";
std::cout << "现在存储的是: " << std::get<std::string>(v) << std::endl;

// 再赋值一个整数
v = 42;
std::cout << "现在存储的是: " << std::get<int>(v) << std::endl;

return 0;
}

输出:

默认值: 0
现在存储的是: Hello
现在存储的是: 42

访问 variant 中的值

有几种方式可以访问 std::variant 中的值:

1. 使用 std::get<类型>

cpp
std::variant<int, std::string> v = "Hello";

try {
std::cout << std::get<int>(v) << std::endl; // 会抛出异常,因为当前存储的是 std::string
}
catch (const std::bad_variant_access& e) {
std::cout << "异常: " << e.what() << std::endl;
}

std::cout << std::get<std::string>(v) << std::endl; // 正常访问

输出:

异常: bad variant access
Hello

2. 使用 std::get<索引>

cpp
std::variant<int, std::string> v = "Hello";

// 使用索引访问,0 代表第一个类型 int,1 代表第二个类型 std::string
// std::cout << std::get<0>(v) << std::endl; // 抛出异常
std::cout << std::get<1>(v) << std::endl; // 打印 "Hello"

3. 使用 std::get_if

std::get_if 提供了一种不会抛出异常的访问方式,如果类型不匹配,它会返回 nullptr。

cpp
std::variant<int, std::string> v = 42;

if (auto pval = std::get_if<int>(&v)) {
std::cout << "整数值: " << *pval << std::endl;
} else if (auto pval = std::get_if<std::string>(&v)) {
std::cout << "字符串值: " << *pval << std::endl;
}

输出:

整数值: 42

检查当前类型

使用 index() 方法可以获得当前存储的类型的索引:

cpp
std::variant<int, std::string, double> v = 3.14;
std::cout << "当前类型索引: " << v.index() << std::endl; // 输出 2,因为 double 是第三个类型

使用 std::holds_alternative 可以检查 variant 是否包含特定类型:

cpp
std::variant<int, std::string> v = 42;

if (std::holds_alternative<int>(v)) {
std::cout << "v 包含一个 int 值" << std::endl;
} else if (std::holds_alternative<std::string>(v)) {
std::cout << "v 包含一个 string 值" << std::endl;
}

输出:

v 包含一个 int 值

访问者模式:std::visit

std::visit 是一个非常强大的函数,它允许你以类型安全的方式处理 variant 中的所有可能类型:

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

int main() {
std::variant<int, std::string, double> v;

// 设置不同的值并访问
v = 42;
std::visit([](auto&& arg) {
std::cout << "值: " << arg << std::endl;
}, v);

v = "Hello";
std::visit([](auto&& arg) {
std::cout << "值: " << arg << std::endl;
}, v);

v = 3.14;
std::visit([](auto&& arg) {
std::cout << "值: " << arg << std::endl;
}, v);

return 0;
}

输出:

值: 42
值: Hello
值: 3.14

你也可以为每个类型定义不同的行为:

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

struct Visitor {
void operator()(int i) const {
std::cout << "整数: " << i << std::endl;
}

void operator()(const std::string& s) const {
std::cout << "字符串: " << s << std::endl;
}

void operator()(double d) const {
std::cout << "浮点数: " << d << std::endl;
}
};

int main() {
std::variant<int, std::string, double> v;

v = 42;
std::visit(Visitor{}, v);

v = "Hello";
std::visit(Visitor{}, v);

v = 3.14;
std::visit(Visitor{}, v);

return 0;
}

输出:

整数: 42
字符串: Hello
浮点数: 3.14

实际应用场景

1. 处理不同类型的配置值

假设你正在编写一个配置系统,不同的配置项可能是不同的类型:

cpp
#include <iostream>
#include <variant>
#include <string>
#include <unordered_map>

// 配置值可以是布尔值、整数、浮点数或字符串
using ConfigValue = std::variant<bool, int, double, std::string>;

class Configuration {
private:
std::unordered_map<std::string, ConfigValue> settings;

public:
template<typename T>
void set(const std::string& key, const T& value) {
settings[key] = value;
}

template<typename T>
T get(const std::string& key) const {
return std::get<T>(settings.at(key));
}

bool has(const std::string& key) const {
return settings.find(key) != settings.end();
}

void print() const {
for (const auto& [key, value] : settings) {
std::cout << key << " = ";
std::visit([](auto&& arg) {
std::cout << arg;
}, value);
std::cout << std::endl;
}
}
};

int main() {
Configuration config;

config.set("server_name", "production_server");
config.set("port", 8080);
config.set("debug_mode", false);
config.set("timeout", 30.5);

std::cout << "配置项:" << std::endl;
config.print();

std::cout << "\n获取单个配置:" << std::endl;
std::cout << "端口: " << config.get<int>("port") << std::endl;
std::cout << "服务器名称: " << config.get<std::string>("server_name") << std::endl;

return 0;
}

输出类似:

配置项:
debug_mode = 0
port = 8080
server_name = production_server
timeout = 30.5

获取单个配置:
端口: 8080
服务器名称: production_server

2. 实现状态机

std::variant 非常适合实现状态机,每个状态可以携带不同的数据:

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

// 定义不同的状态及其相关数据
struct Idle {};

struct Loading {
int progress;
};

struct Success {
std::string data;
};

struct Failed {
std::string error_message;
};

// 使用 variant 表示状态
using NetworkState = std::variant<Idle, Loading, Success, Failed>;

// 状态处理函数
void handle_state(const NetworkState& state) {
std::visit([](auto&& s) {
using T = std::decay_t<decltype(s)>;

if constexpr (std::is_same_v<T, Idle>) {
std::cout << "空闲状态:等待请求" << std::endl;
}
else if constexpr (std::is_same_v<T, Loading>) {
std::cout << "加载中:已完成 " << s.progress << "%" << std::endl;
}
else if constexpr (std::is_same_v<T, Success>) {
std::cout << "成功:收到数据 \"" << s.data << "\"" << std::endl;
}
else if constexpr (std::is_same_v<T, Failed>) {
std::cout << "失败:" << s.error_message << std::endl;
}
}, state);
}

int main() {
NetworkState state = Idle{};
handle_state(state);

state = Loading{50};
handle_state(state);

state = Success{"Hello, World!"};
handle_state(state);

state = Failed{"Connection timed out"};
handle_state(state);

return 0;
}

输出:

空闲状态:等待请求
加载中:已完成 50%
成功:收到数据 "Hello, World!"
失败:Connection timed out

注意事项与限制

  1. 空状态std::variant 不能为空,它始终包含其中一个可能的类型的值。

  2. 类型不能为 void, reference, 数组和 function 类型:这些类型在 variant 中不被支持。

  3. 存储 monostate:如果你需要一个可能为"空"的变体,可以使用 std::monostate

cpp
#include <variant>
#include <iostream>

int main() {
std::variant<std::monostate, int, std::string> v;

if (std::holds_alternative<std::monostate>(v)) {
std::cout << "variant 目前是空的" << std::endl;
}

v = 42;
std::cout << "现在存储的是: " << std::get<int>(v) << std::endl;

return 0;
}
  1. 异常情况:当 variant 无法容纳新值时(例如,内存不足),它会进入"valueless by exception"状态:
cpp
#include <variant>
#include <iostream>
#include <string>

class ThrowOnCopy {
public:
ThrowOnCopy() = default;
ThrowOnCopy(const ThrowOnCopy&) { throw std::runtime_error("Copy failed"); }
};

int main() {
std::variant<int, ThrowOnCopy> v = 5;

try {
ThrowOnCopy t;
v = t; // 会引发异常
} catch (const std::exception& e) {
std::cout << "捕获异常: " << e.what() << std::endl;
std::cout << "variant 有效吗: " << (!v.valueless_by_exception() ? "是" : "否") << std::endl;
}

return 0;
}

总结

std::variant 是 C++17 引入的一个强大的类型,它提供了一种类型安全的方式来在单一变量中存储多种可能的类型。它比传统的 union 更安全,比基类指针更轻量级,没有动态内存分配的开销。

主要优点包括:

  • 类型安全
  • 不需要动态内存分配
  • 可以存储任意类型的组合
  • 提供多种访问方式(std::get, std::get_if, std::visit
  • 特别适合实现状态机、解析器、配置系统等

练习

  1. 创建一个 std::variant 来存储整数、浮点数和字符串,并实现一个简单的计算器,根据输入的类型执行不同的操作。

  2. 使用 std::variantstd::visit 实现一个简单的图形系统,能够存储和绘制不同类型的图形(圆形、矩形、三角形等)。

  3. 修改上面的配置系统示例,添加配置项类型验证,如果尝试获取错误类型,提供适当的错误处理机制。

参考资源

通过学习和掌握 std::variant,你将能够更加灵活地处理需要多种类型的情况,编写更加类型安全和可维护的代码。