C++ 17 std::variant
什么是 std::variant?
std::variant
是 C++17 引入的一个新的标准库类型,它允许你在一个变量中存储多种可能的类型中的一个。你可以把它理解为一个类型安全的联合体(union)。与传统的 union
不同,std::variant
知道它当前存储的是哪种类型,并提供了类型安全的访问方式。
std::variant
定义在 <variant>
头文件中,属于 std
命名空间。
为什么需要 std::variant?
在 C++17 之前,如果你想在一个变量中存储多种可能的类型,你可能会使用:
union
:但它不能安全地处理有构造函数和析构函数的类型- 基类指针和继承:需要动态内存分配,且类型必须相关
void*
:完全丧失了类型安全
std::variant
解决了这些问题,它是类型安全的,支持任意类型组合,不需要动态内存分配。
基本用法
创建和赋值
#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<类型>
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<索引>
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。
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()
方法可以获得当前存储的类型的索引:
std::variant<int, std::string, double> v = 3.14;
std::cout << "当前类型索引: " << v.index() << std::endl; // 输出 2,因为 double 是第三个类型
使用 std::holds_alternative
可以检查 variant 是否包含特定类型:
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 中的所有可能类型:
#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
你也可以为每个类型定义不同的行为:
#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. 处理不同类型的配置值
假设你正在编写一个配置系统,不同的配置项可能是不同的类型:
#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
非常适合实现状态机,每个状态可以携带不同的数据:
#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
注意事项与限制
-
空状态:
std::variant
不能为空,它始终包含其中一个可能的类型的值。 -
类型不能为 void, reference, 数组和 function 类型:这些类型在 variant 中不被支持。
-
存储 monostate:如果你需要一个可能为"空"的变体,可以使用
std::monostate
:
#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;
}
- 异常情况:当 variant 无法容纳新值时(例如,内存不足),它会进入"valueless by exception"状态:
#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
) - 特别适合实现状态机、解析器、配置系统等
练习
-
创建一个
std::variant
来存储整数、浮点数和字符串,并实现一个简单的计算器,根据输入的类型执行不同的操作。 -
使用
std::variant
和std::visit
实现一个简单的图形系统,能够存储和绘制不同类型的图形(圆形、矩形、三角形等)。 -
修改上面的配置系统示例,添加配置项类型验证,如果尝试获取错误类型,提供适当的错误处理机制。
参考资源
通过学习和掌握 std::variant
,你将能够更加灵活地处理需要多种类型的情况,编写更加类型安全和可维护的代码。