【C++现代#12】C++17 三大值语义容器详解:std::optional、std::variant 与 std::any
是 C++17 引入的一种类型安全的联合体(Union),它允许可变类型的数据存储在一个统一的类型中。传统联合体(union)的问题:C风格或 C++的普通 union 不是类型安全的。我们需要自己记住当前存储的是哪种类型,如果访问错了(比如在一个存储 int 的 union 上读取 float),会导致未定义行为,而且它无法处理非平凡类型(如 std::string)std::variant 的

📃个人主页:island1314
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
🔥 目录
一、 std::optional
std::optional 是 C++17 引入的一个模板类,它用于表示一个可能包含值或者不包含值(即“无值”状态)的容器。
为什么需要 std::optional?
在程序设计中,我们经常会遇到一些场景,某个变量可能有值也可能没有值,通常就会用如下方式处理 “可能无返回值” 情况:
- 返回特殊值:例如 find 函数返回 -1 或
string::npos,返回 nullptr 指针等。问题在于:这些特殊值没有类型安全保证,调用者很容易忘记检查,代码可读性差。 - 使用输出参数:通过引用传递参数来存储结果,函数本身返回一个bool 表示成功与否。语法笨拙,不够直观。
- 抛出异常:并非所有“无结果”的情况都是异常,有时它只是一个正常的、可预期的分支。使用异常来处理控制流可能开销较大且不直观。
但这些方法存在一定的缺陷,比如空指针解引用会导致程序崩溃,标记值可能和正常值冲突等。而 std::optional 提供了一种更安全、更明确的方式来处理这种“可选值”场景,它将值(或有或无)包装在⼀个类型中,强制调用者处理可能无值的情况
常用接口
| 特性 | 说明 | 代码示例 |
|---|---|---|
| 创建空 | 表示无值 | std::optionalempty; auto empty = std::nullopt; |
| 创建有值 | 包装一个值 | std::optionalopt = 5; auto opt = std::make_optional(5); |
| 检查 | 判断是否包含值 | if(opt.has_value()){…} if(opt){…} |
| 安全取值 | 有值返回值,无值抛异常 | int x = opt.value(); |
| 安全取值(带默认) | 无值时返回默认值 | int x = opt.value_or(0); |
| 不安全取值 | 必须确保有值,否则 UB | int x = *opt; |
| 重置 | 使其变为空 | opt.reset(); opt=std::nullopt; |
std::optional 的主要特性
-
表示有值或无值状态:
std::optional可以明确地表示一个值是否存在。通过成员函数has_value()可以检查是否有值,返回true表示有值,false表示无值。 -
安全地访问值:如果
std::optional对象有值,可以通过value()成员函数访问它;如果无值,调用value()会抛出异常std::bad_optional_access。此外,还可以使用operator*和operator->来访问值,这与智能指针的用法类似。 -
避免空指针问题:与使用裸指针表示无值不同,
std::optional不会因为解引用空指针而导致程序崩溃。它将无值状态封装在类型系统中,使得对值的访问必须先进行检查。
示例
#include <iostream>
#include <vector>
#include <map>
#include <optional>
// 结合文⽂档,optional的基本使用
void test_example1(){
// 1、定义optional 对象
std::optional<int> maybeInt; // 初始为空
std::optional<std::string> maybeString = "Hello"; // 初始有值
std::optional<double> empty = std::nullopt; // 显式设置为空
std::optional<double> opt = std::make_optional(3.14); // 使用 std::make_optional 创建对象
// 2、检查是否有值
if (maybeInt.has_value()) {
std::cout << "has_value1: " << *maybeInt << std::endl;
}
// 或者更简洁的写法
if (maybeString) {
std::cout << "has_value2: " << *maybeString << std::endl;
}
// 3、尝试访问无值的 optional
try {
int value = maybeInt.value();
}catch (const std::bad_optional_access& e) {
std::cout << e.what() << std::endl; // 无值时抛出 std::bad_optional_access
}
// 安全取值
maybeInt = 1;
std::cout << "maybeInt has value: " << maybeInt.value_or(-1) << std::endl;
// 不安全但快速的访问 - 无值时行为未定义
int value1 = *maybeInt;
// 带默认值的访问
int value2 = maybeInt.value_or(2); // ⽆值时返回2
std::cout << "value2: " << value2 << std::endl;
// 4、修改值
maybeInt = 42; // 赋新值
maybeInt = std::nullopt; // 设为空
maybeInt.reset(); // 设为空
}
// optional实践中的使用场景
void test_example2(){
// 查找⼀个顶点对应的下标编号
std::map<std::string, int> indexMap = { {"张庄",1}, {"王村",2}, {"李家村",3}, {"王家坪",3} };
auto findIndex = [&indexMap](const std::string& str)->std::optional<int>
{
auto it = indexMap.find(str);
if (it != indexMap.end()){
return it->second;
}else{
return std::nullopt;
}
};
std::string x;
std::cin >> x;
std::optional<int> index = findIndex(x);
if (index){
std::cout << x << "对应的编号为:" << *index << std::endl;
}else{
std::cout << x << "是非法顶点" << std::endl;
}
std::vector<std::string> v = { "张庄", "李庄", "" };
auto access = [&v](int i)-> std::optional<std::string>
{
if (i < v.size()){
return v[i];
}else{
// 以前的语法这里函数只能⽤抛异常或者断⾔⽅式处理i越界的情况
// i越界不能返回"",因为正常数据可能就是"",⽆法区分
std::nullopt;
}
};
}
int main(){
test_example1();
test_example2();
return 0;
}
使用场景
- 函数返回值:当函数可能成功返回一个值,也可能不返回任何值时,
std::optional是一个很好的选择。例如,从集合中查找一个元素,如果找到就返回该元素,否则返回无值。 - 配置参数:在某些配置场景中,某些参数可能是可选的。使用
std::optional可以清晰地表示这些参数是否有值。 - 避免特殊标记值:当不想使用特殊的标记值(如 -1、空字符串等)来表示无值状态时,
std::optional提供了更语义化的方式。
注意事项
-
类型限制:
std::optional的模板参数类型不能是不完全类型(incomplete type),并且不能是std::in_place_t类型。 -
性能开销:虽然
std::optional提供了安全性,但它可能会带来一些性能开销,因为它需要存储额外的状态信息(表示是否有值)。在性能敏感的场景中,需要权衡其使用是否合适。 -
与指针的区别:
std::optional和指针都可以表示无值状态,但std::optional将值存储在其内部,而指针指向外部存储。对于小对象或频繁使用的值,std::optional可能更高效;对于大对象或需要动态分配的情况,指针可能更合适。
二、 std::variant
1. 基本概述
std::variant 是 C++17 引入的一种类型安全的联合体(Union),它允许可变类型的数据存储在一个统一的类型中。
- 传统联合体(union)的问题:C风格或 C++的普通 union 不是类型安全的。
- 我们需要自己记住当前存储的是哪种类型,如果访问错了(比如在一个存储 int 的 union 上读取 float),会导致未定义行为,而且它无法处理非平凡类型(如 std::string)
- std::variant 的优势是它解决了所有这些问题。它自己知道当前存储的是哪种类型,并确保对象被正确构造和析构,而且能够有效防止非法类型转换和访问越界等问题,可以把它想象成一个 “智能的” “类型丰富的” union。
核心特性
- 类型安全:通过编译期检查,确保对
std::variant中存储的数据的访问是合法的,防止类型越界等问题。 - 可变类型存储:支持在一个变量中存储多种不同类型的值。
- 访问控制:提供多种访问方式,如
std::get、std::visit等,确保安全地访问存储的值。
使用场景
- 类型不确定的场景:当函数返回值或数据类型不确定时,
std::variant可以提供统一的接口。 - 可选值场景:在需要表示无值或多种可能值的情况下,
std::variant是一个很好的选择。 - 类型安全的联合体:传统联合体类型不安全,容易导致类型越界等问题,
std::variant提供了更安全的替代方案。
2. 定义和访问
#include <iostream>
#include <variant>
#include <string>
int main() {
std::variant<int, double, std::string> v;
v = 42; // 现在持有 int
std::cout << "int: " << std::get<int>(v) << std::endl;
v = 3.14; // 现在持有 double
std::cout << "double: " << std::get<double>(v) << std::endl;
v = "hello"; // 现在持有 string
std::cout << "string: " << std::get<std::string>(v) << std::endl;
// 赋值时找不到对应类型的值则报错
// v = std::pair<int, int>{}; // Error
// 使用 index() 获取当前持有的类型索引
std::cout << "Current Index: " << v.index() << std::endl;
return 0;
}
注意:std::variant 是一个类型安全的联合体,它要求其模板参数中的类型是唯一的。如果试图在 std::variant 中存储两个相同类型的值,编译器会报错,因为这违反了 std::variant 的设计原则。
std::variant<std::string,std::string> v2;
v2 = "abc"; // Error
解决:使用 emplace
int main() {
std::variant<std::string, std::string> v2;
v2.emplace<1>("ghi");
// v2.emplace<std::string>("abc"); // Error
std::cout << std::get<1>(v2) << "\n";
std::variant<std::string> v3;
v3.emplace<0>("abc");
std::cout << std::get<0>(v3) << "\n";
v3.emplace<std::string>("abc");
std::cout << std::get<0>(v3) << "\n";
return 0;
}
3. 访问值
方式一:使用 std:get<Type>或 std:.get<index> 可以通过类型或索引来直接获取值
- 但如果当前variant 存储的不是你请求的类型/索引,它会抛出
std::bad_variant_access异常。
int main() {
std::variant<int, double> v = 42;
try {
std::cout << std::get<int>(v) << std::endl;
std::cout << std::get<0>(v) << std::endl;
std::cout << std::get<double>(v) << std::endl; // 抛出异常
}catch (const std::bad_variant_access& e) {
std::cout << "Error: " << e.what() << std::endl;
}
}
方式二:使用 std:get_if<Type> std::get_if 不会抛出异常
- 它接受一个指针参数,如果
variant当前存储的是指定类型,则返回一个指向该值的指针;否则返回nullptr
int main() {
std::variant<int, double, std::string> v = "hello";
// 使⽤std::get_if尝试获取值
if (auto pval = std::get_if<int>(&v)) {
std::cout << "int value: " << *pval << std::endl;
}else if (auto pval = std::get_if<double>(&v)) {
std::cout << "double value: " << *pval << std::endl;
}else if (auto pval = std::get_if<std::string>(&v)) {
std::cout << "string value: " << *pval << std::endl;
}
}
方式三:使用 std::visit(推荐,最安全强大)std::visit 允许你提供一个“访问者”(visitor)来根据当前存储的类型执行相应的操作,这是最类型安全、最清晰的方式。
语法
template <class Visitor, class... Variants>
constexpr auto visit(Visitor&& vis, Variants&&... vars);
- Visitor:访问者类型,通常是一个重载了
operator()的类或 lambda 表达式。 - Variants:一个或多个
std::variant对象。
示例
#include <iostream>
#include <variant>
#include <string>
// 定义一个访问者类
struct Visitor {
// 重载 operator() 对 int 类型
void operator()(int value) const {
std::cout << "Integer: " << value << ", Size: " << sizeof(value) << " bytes" << std::endl;
}
// 重载 operator() 对 double 类型
void operator()(double value) const {
std::cout << "Double: " << value << ", Size: " << sizeof(value) << " bytes" << std::endl;
}
// 重载 operator() 对 std::string 类型
void operator()(const std::string& value) const {
std::cout << "String: " << value << ", Length: " << value.length() << std::endl;
}
};
int main() {
// 创建一个 std::variant 对象
std::variant<int, double, std::string> v;
// 赋值为 int 类型
v = 42;
// 使用 std::visit 和自定义访问者类
std::visit(Visitor{}, v);
// 赋值为 double 类型
v = 3.14;
std::visit(Visitor{}, v);
// 赋值为 std::string 类型
v = std::string("Hello, World!"); // 使用 std::string 构造函数
std::visit(Visitor{}, v);
// 使用 lambda 表达式作为访问者
std::visit([](auto&& arg) {
if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, int>) {
std::cout << "Lambda - Integer: " << arg << ", Size: " << sizeof(arg) << " bytes" << std::endl;
}else if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, double>) {
std::cout << "Lambda - Double: " << arg << ", Size: " << sizeof(arg) << " bytes" << std::endl;
}else if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, std::string>) {
std::cout << "Lambda - String: " << arg << ", Length: " << arg.length() << std::endl;
}
}, v);
// 对多个 std::variant 进行操作
std::variant<int, double> v1 = 100;
std::variant<int, double> v2 = 200.5;
std::visit([](const auto& a, const auto& b) {
std::cout << "Sum: " << a + b << std::endl;
}, v1, v2);
return 0;
}
注意事项
- 访问者必须覆盖所有类型:访问者需要为
std::variant中定义的所有类型提供对应的operator()重载,否则会导致编译错误。 - 性能开销:
std::visit的调用涉及函数指针调用或模板实例化,可能会带来一定的性能开销。在性能敏感的场景中需要权衡使用。 - 编译器支持:确保你的编译器支持 C++17 或更高版本。
示例:实战实现哈希桶
#include <iostream>
#include <variant>
#include <vector>
#include <list>
#include <set>
template<class ...Ts>
struct overloaded : Ts ...{using Ts::operator()...; };
template<class ...Ts>
overloaded(Ts ...)->overloaded<Ts ...>;
template<class K, size_t Len = 8>
class HashTable {
using Value = std::variant<std::list<K>, std::set<K>>;
public:
HashTable(): _t(10){}
void Insert(const K& key) {
size_t index = key % _t.size();
// 链表插入
auto listInsert = [this, &key, index](std::list<K>& lt) {
if (lt.size() < Len) lt.push_back(key);
else {
// 大于 8 转换插入到红黑树 set
std::set<K> s(lt.begin(), lt.end());
s.insert(key);
_t[index] = move(s);
}
};
// 红黑树插入
auto setInsert = [&key](std::set<K>& s) {
s.insert(key);
};
std::visit(overloaded{ listInsert, setInsert }, _t[index]);
}
bool Find(const K& key){
size_t index = key % _t.size();
// 链表查找
auto listFind = [&key](std::list<K>& lt) ->bool{
return std::find(lt.begin(), lt.end(), key) != lt.end();
};
// 红黑树查找
auto setFind = [&key](std::set<K>& s) ->bool{
return s.count(key);
};
return std::visit(overloaded{listFind,setFind}, _t[index]);
}
private:
std::vector<Value> _t;
};
int main(){
HashTable<int> ht;
for (size_t i = 0; i < 10; i++){
ht.Insert(i * 10 + 5);
}
std::cout << ht.Find(15) << std::endl;
std::cout << ht.Find(3) << std::endl;
return 0;
}
三、std::any
1. 基本概述
std::any 是 C++17 引入的一个类型安全的通用容器,可以存储 任何类型(必须是可拷贝构造的) 单个值的容器
-
当我们从 any 中取出值时,我们须知道其 原始类型,并通过
std::any_cast进行安全的转换。如果类型不匹配,它会 抛异常 或 返回空指针。 -
与原始的
void*不同,any 会记录类型信息,并在 any_cast 时进行检查。其次它管理着自己内部存储的对象生命周期(构造、拷贝、析构),为了避免为小对象频繁分配堆内存,许多实现会使用一个 小缓冲区优化(SB0,Small BufferOptimization),MSVC下的std:string就使用了这个优化。 -
它类似于
std::variant,但与std::variant不同的是,std::variant在编译期就确定了可以存储的类型列表,而std::any可以存储任意类型的数据。
核心特性
- 存储任意类型:可以存储任何类型的数据,包括内置类型和自定义类型。
- 类型安全:提供类型检查机制,确保对存储数据的访问是合法的。
- 动态类型查询:可以通过
type()成员函数查询存储的数据类型。 - 值访问:通过
any_cast安全地访问存储的值。
使用场景
- 存储未知类型的数据:在需要存储类型不确定的数据时,
std::any提供了一个通用的解决方案。 - 函数返回值:当函数可能返回多种不同类型的值时,
std::any可以提供一个统一的返回类型。 - 配置参数:在配置文件或参数系统中,
std::any可以用于存储各种类型的配置值。
std::any 的接口非常简洁,主要包含以下成员函数:
| 函数 | 作用 | 示例代码 |
|---|---|---|
| 构造函数 | 创建一个 std::any 对象,可以初始化为任意类型的值。 | std::any any_value = 42;std::any any_value = std::string("Hello"); |
| operator= | 给 std::any 对象赋值为任意类型的值。 | any_value = 3.14;any_value = std::vector<int>{1, 2, 3}; |
| emplace(args…) | 原地构造一个类型为 T 的对象,参数 args 传递给 T 的构造函数。 | any_value.emplace<std::string>("World");any_value.emplace<int>(42); |
| reset() | 销毁内部包含的对象,使 std::any 变为空。 | any_value.reset(); |
| has_value() | 返回一个 bool,判断 std::any 对象当前是否包含一个值。 | if (any_value.has_value()) { ... } |
| type() | 返回一个 std::type_info const&,表示当前包含值的类型。如果 std::any 为空,则返回 typeid(void)。 | std::cout << any_value.type().name(); |
| std::any_cast | 用于从 std::any 对象中提取值。如果转换失败,会抛出 std::bad_any_cast 异常。 | int value = std::any_cast<int>(any_value);std::string str = std::any_cast<std::string>(any_value); |
2. 使用
示例一:定义和访问
#include <iostream>
#include <any>
#include <string>
#include <typeindex>
#include <typeinfo>
int main() {
// 1. 创建一个 std::any 对象
std::any any1 = 42; // 存储 int
std::any any2 = 3.14; // 存储 double
std::any any3 = std::string("Hello, World!"); // 存储 string
std::any any4 = std::pair<std::string, std::string>("xxx", "yyy"); // 存储 pair
// 2. 获取类型信息
std::cout << "any1: " << std::any_cast<int>(any1) << ", Type: " << typeid(any1).name() << std::endl; // class std::any
const std::type_info& ti = any1.type();
std::cout << "any1: " << std::any_cast<int>(any1) << ", Type: " << ti.name() << std::endl; // int
// 3. 检查类型并访问值
if (any3.type() == typeid(std::string)) {
std::string value = std::any_cast<std::string>(any3);
std::cout << "any3 cast to std::string: " << value << std::endl; // Hello, World!
}else {
std::cerr << "Type mismatch!" << std::endl;
}
// 4. 尝试访问错误的类型
try {
int value = std::any_cast<int>(any3);
}catch (const std::bad_any_cast& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
// 5. 使用 emplace 原地构造
any3.emplace<std::string>("world");
// 6. 检查是否有值
if (any3.has_value()) {
std::cout << "a has value" << std::endl;
}
return 0;
}
还记得上面我说过,他这里用到了优化,看看如下代码就知道了
std::cout << sizeof(any1) << "\n"; // 64
std::cout << sizeof(any2) << "\n"; // 64
然后调试可以看到其底层的数据结构

示例二:取值
#include <iostream>
#include <any>
#include <string>
#include <vector>
#include <cassert>
// vector中存储any类型
void anyVector() {
std::string str("hello world");
std::vector<std::any> v = { 1.1, 2, str };
for (const auto& item : v) {
if (item.type() == typeid(int)) {
std::cout << "整数配置: " << std::any_cast<int>(item) << '\n';
}
else if (item.type() == typeid(double)) {
std::cout << "浮点配置: " << std::any_cast<double>(item) << '\n';
}
else if (item.type() == typeid(std::string)) {
// 移除 const 属性以支持引用访问
std::cout << "字符串配置: " << std::any_cast<std::string&>(const_cast<std::any&>(item)) << '\n';
}
else {
assert(false);
}
}
}
int main() {
std::any a1 = 42; // 存放 int
std::any a2 = 3.14; // 存放 double
std::any a3 = std::string("Hello"); // 存放 std::string
// 方式一:转换为值的类型(如果类型不匹配,抛出 std::bad_any_cast)
try {
int int_value = std::any_cast<int>(a1); // 正确,a1 存放的是 int
std::cout << "Value: " << int_value << '\n';
double double_value = std::any_cast<double>(a1); // 错误!抛出异常
}catch (const std::bad_any_cast& e) {
std::cout << "Cast failed: " << e.what() << '\n';
}
// 方式二: 转换为值和转换为值的引用
// 这里 any_cast 返回的是 a3 存储对象的拷贝,要尽量避免这样使用
std::string str_ref1 = std::any_cast<std::string>(a3);
str_ref1[0]++;
std::cout << str_ref1 << '\n'; // 输出 "Iello"
std::string& str_ref2 = std::any_cast<std::string&>(const_cast<std::any&>(a3));
str_ref2[0]++; // 对原值产生影响
std::cout << std::any_cast<std::string&>(const_cast<std::any&>(a3)) << '\n'; // 输出 "Iello"
// 方式三: 转换为指针(如果类型不匹配,返回 nullptr,不会抛异常)
if (auto ptr = std::any_cast<int>(&a1)) { // 传递指针,返回指针
std::cout << "Value via pointer: " << *ptr << '\n';
}else {
std::cout << "Not an int or is empty.\n";
}
anyVector();
return 0;
}
注意事项
- 类型检查:访问
std::any中的值时,必须进行类型检查,否则会引发std::bad_any_cast异常。 - 性能开销:
std::any的类型检查和动态类型信息存储会带来一定的性能开销。 - 与
std::variant的区别:std::variant在编译期确定可以存储的类型列表,而std::any可以存储任意类型;std::variant更适合存储有限的几种类型,std::any更适合存储未知或动态类型。
3. 对比 variant
| 特性 | std::any |
std::variant |
|---|---|---|
| 类型安全性 | 运行时检查类型,any_cast 失败抛出异常。 |
编译期检查类型,不匹配直接编译错误。 |
| 存储类型 | 可以存储任意类型。 | 必须在模板参数中显式指定所有可能的类型。 |
| 类型查询 | 提供 type() 方法查询当前存储类型的 std::type_info。 |
不直接提供类型查询方法,类型检查在编译期完成。 |
| 性能开销 | 由于动态类型信息,可能稍高。 | 更高效,编译期确定类型,无运行时类型存储开销。 |
| 适用场景 | 适用于需要存储未知类型或动态类型的场景。 | 适用于已知可能类型的有限集合,类型安全更重要。 |
| 异常处理 | any_cast 失败时抛出 std::bad_any_cast。 |
访问不存在的类型时抛出 std::bad_variant_access。 |
| 存储类型数量 | 无限制。 | 模板参数中类型数量有限(通常不超过 30 个)。 |
| 类型擦除 | 是,将具体类型擦除为通用 std::any。 |
否,类型在编译期明确。 |
【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【C++】的内容,请持续关注我 !!

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)