C++17 之 std::optional 与 std::variant
C++17 之 std::optional 与 std::variant
系列文章: C++ 新特性系列 · 第3篇
阅读时间: 约 15 分钟
适用标准: C++17 及以上
引言:传统 C++ 的两大痛点
在 C++17 之前,我们经常面临两个让人头疼的问题:
问题一:如何表示"没有值"?
查找一个键值对,可能找到,也可能没找到。传统做法是返回指针或使用特殊标记值:
// 方式1:返回指针 —— 悬空指针的风险
int* find(const std::map<std::string, int>& m, const std::string& key);
// 方式2:使用特殊值 —— -1 本身就是合法值怎么办?
int find(const std::map<std::string, int>& m, const std::string& key);
// 方式3:输出参数 —— 代码啰嗦,不够优雅
bool find(const std::map<std::string, int>& m, const std::string& key, int& out);
问题二:如何安全地存储不同类型?
union 可以存储不同类型,但不记录当前存的是哪个,手动管理类型标签既繁琐又容易出错:
union Value {
int i;
double d;
const char* s;
};
// 你永远不知道当前是 int 还是 double,访问错了就是未定义行为
C++17 用两个优雅的工具解决了这两个问题:std::optional 和 std::variant。
std::optional:优雅地表达"可能没有值"
为什么需要 optional?
std::optional<T> 本质上是一个包装了 T 值的容器,它要么包含一个值,要么为空。相比传统方式:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 返回指针 | 简单 | 需管理内存,可能悬空 |
| 特殊标记值 | 无额外开销 | 占用合法值,语义不清 |
| 输出参数 + bool | 兼容老代码 | 写法啰嗦,不直观 |
std::optional<T> |
语义明确,类型安全 | 需 C++17 |
核心 API 速览
#include <optional>
#include <string>
std::optional<int> opt1; // 空 optional
std::optional<int> opt2 = 42; // 包含值 42
std::optional<std::string> opt3 = "hello"; // 包含字符串
// 判断是否有值
if (opt2.has_value()) { ... } // 方式1
if (opt2) { ... } // 方式2,更简洁
// 获取值
int val1 = opt2.value(); // 无值时抛 std::bad_optional_access
int val2 = opt2.value_or(0); // 无值时返回默认值 0
int val3 = *opt2; // 语法糖,无值时行为未定义(别乱用)
示例1:optional 作为函数返回值表示可能失败
这是 std::optional 最常见的使用场景——函数可能返回有效结果,也可能"什么都返回不了"。
#include <iostream>
#include <optional>
#include <string>
#include <map>
// 在 map 中查找值,找不到返回空 optional
std::optional<int> findValue(const std::map<std::string, int>& data,
const std::string& key) {
auto it = data.find(key);
if (it != data.end()) {
return it->second; // 隐式构造 optional<int>
}
return std::nullopt; // 表示"没有值"
}
int main() {
std::map<std::string, int> scores = {
{"Alice", 95}, {"Bob", 87}, {"Charlie", 72}
};
auto result = findValue(scores, "Bob");
if (result) {
std::cout << "Bob's score: " << *result << std::endl;
}
auto missing = findValue(scores, "David");
// 用 value_or 提供默认值,安全又简洁
std::cout << "David's score: " << missing.value_or(-1) << std::endl;
return 0;
}
输出:
Bob's score: 87
David's score: -1
关键点:
return it->second利用optional的隐式构造,无需写std::optional<int>(it->second)。返回std::nullopt比返回-1或nullptr语义清晰得多。
示例2:optional 的链式操作
C++23 之前,std::optional 的链式操作需要手动处理,但配合辅助函数也能写出优雅的代码:
#include <iostream>
#include <optional>
#include <string>
#include <sstream>
struct User {
std::string name;
std::optional<std::string> email;
std::optional<std::string> phone;
};
// 辅助函数:如果 optional 有值,对其进行变换
template<typename T, typename F>
auto optionalMap(const std::optional<T>& opt, F&& func)
-> std::optional<decltype(func(*opt))> {
if (opt) {
return func(*opt);
}
return std::nullopt;
}
// 模拟从数据库获取用户(可能失败)
std::optional<User> findUser(int id) {
if (id == 1) {
return User{"Alice", "alice@example.com", "13800138000"};
}
if (id == 2) {
return User{"Bob", std::nullopt, std::nullopt}; // Bob 没有邮箱和电话
}
return std::nullopt;
}
int main() {
auto user = findUser(1);
// 链式获取:用户 -> 邮箱 -> 邮箱用户名部分
auto emailUsername = optionalMap(user, [](const User& u) {
return optionalMap(u.email, [](const std::string& email) {
return email.substr(0, email.find('@'));
});
});
if (emailUsername) {
std::cout << "Email username: " << *emailUsername << std::endl;
}
// 用 value_or 提供默认联系信息
auto contact = user
? (user->email.value_or(user->phone.value_or("无联系方式")))
: "用户不存在";
std::cout << "Contact: " << contact << std::endl;
return 0;
}
输出:
Email username: alice
Contact: alice@example.com
std::variant:类型安全的"万能容器"
为什么需要 variant?
union 的致命问题是类型不安全——你必须自己记住当前存的是什么类型,访问错了就是未定义行为。std::variant<Types...> 完美解决了这个问题:
| 特性 | union | std::variant |
|---|---|---|
| 类型安全 | ❌ 可能 UB | ✅ 编译期检查 |
| 自动类型标签 | ❌ 手动管理 | ✅ 内置 index() |
| 析构函数调用 | ❌ 需手动 | ✅ 自动调用 |
| 访问方式 | 直接强转 | get<>() / visit() |
| 空状态 | ✅ 可以为空 | ❌ 必须存一个值 |
核心 API 速览
#include <variant>
#include <string>
std::variant<int, double, std::string> v; // 默认存第一个类型(int, 值为0)
// 赋值不同类型
v = 3.14; // 现在存 double
v = "hello"; // 现在存 std::string(C++17 用 const char* 隐式转换)
// 查询当前类型
size_t idx = v.index(); // 当前存的是第几个类型(从0开始)
bool ok = std::holds_alternative<std::string>(v); // 是否是 string
// 获取值
auto& s = std::get<std::string>(v); // 引用获取,类型不对抛 std::bad_variant_access
auto* p = std::get_if<std::string>(&v); // 指针获取,类型不对返回 nullptr
示例3:variant 存储不同类型
std::variant 最典型的场景是需要存储多种可能类型的值,比如 JSON 值、AST 节点等:
#include <iostream>
#include <variant>
#include <string>
#include <vector>
// 模拟一个简单的 JSON 值类型
using JsonValue = std::variant<
std::nullptr_t, // null
bool, // boolean
int, // integer
double, // number
std::string, // string
std::vector<int> // array(简化为 int 数组)
>;
void printJson(const JsonValue& val) {
// std::visit + 重载模式,优雅地处理所有类型
std::visit([](const auto& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::nullptr_t>) {
std::cout << "null";
} else if constexpr (std::is_same_v<T, bool>) {
std::cout << (arg ? "true" : "false");
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "\"" << arg << "\"";
} else if constexpr (std::is_same_v<T, std::vector<int>>) {
std::cout << "[";
for (size_t i = 0; i < arg.size(); ++i) {
if (i > 0) std::cout << ", ";
std::cout << arg[i];
}
std::cout << "]";
} else {
std::cout << arg; // int 和 double 走这里
}
}, val);
std::cout << std::endl;
}
int main() {
std::vector<JsonValue> jsonArray = {
nullptr,
true,
42,
3.14,
std::string("Hello, C++17!"),
std::vector<int>{1, 2, 3, 4, 5}
};
for (const auto& val : jsonArray) {
std::cout << "Type: " << val.index()
<< ", Value: ";
printJson(val);
}
return 0;
}
输出:
Type: 0, Value: null
Type: 1, Value: true
Type: 2, Value: 42
Type: 3, Value: 3.14
Type: 4, Value: "Hello, C++17!"
Type: 5, Value: [1, 2, 3, 4, 5]
示例4:visit 访问 variant
std::visit 是访问 variant 的推荐方式,配合重载模式(overloaded pattern)可以写出非常优雅的代码:
#include <iostream>
#include <variant>
#include <string>
#include <vector>
// 定义一些不同的"形状"
struct Circle { double radius; };
struct Rectangle { double width, height; };
struct Triangle { double base, height; };
using Shape = std::variant<Circle, Rectangle, Triangle>;
// 计算面积
double area(const Shape& shape) {
return std::visit([](const auto& s) -> double {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Circle>) {
return 3.14159265 * s.radius * s.radius;
} else if constexpr (std::is_same_v<T, Rectangle>) {
return s.width * s.height;
} else if constexpr (std::is_same_v<T, Triangle>) {
return 0.5 * s.base * s.height;
}
}, shape);
}
// 重载模式:把多个 lambda 合并成一个 visitor
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
// 用重载模式获取形状描述
std::string describe(const Shape& shape) {
return std::visit(overloaded{
[](const Circle& c) {
return "Circle(r=" + std::to_string(c.radius) + ")";
},
[](const Rectangle& r) {
return "Rect(" + std::to_string(r.width) + "x"
+ std::to_string(r.height) + ")";
},
[](const Triangle& t) {
return "Tri(b=" + std::to_string(t.base)
+ ",h=" + std::to_string(t.height) + ")";
}
}, shape);
}
int main() {
std::vector<Shape> shapes = {
Circle{5.0},
Rectangle{4.0, 6.0},
Triangle{3.0, 8.0}
};
for (const auto& shape : shapes) {
std::cout << describe(shape)
<< " -> Area: " << area(shape) << std::endl;
}
return 0;
}
输出:
Circle(r=5.000000) -> Area: 78.539816
Rect(4.000000x6.000000) -> Area: 24.000000
Tri(b=3.000000,h=8.000000) -> Area: 12.000000
重载模式小技巧:
overloaded继承了所有 lambda 的operator(),配合 CTAD(C++17 类模板参数推导),无需手动指定模板参数。
注意事项与常见陷阱
std::optional 的陷阱
| 陷阱 | 后果 | 正确做法 |
|---|---|---|
对空 optional 调用 value() |
抛 std::bad_optional_access |
先 has_value() 检查,或用 value_or() |
对空 optional 使用 operator* |
未定义行为 | 同上 |
optional<T> 占用额外空间 |
sizeof(optional<T>) > sizeof(T) |
性能敏感场景考虑替代方案 |
| 返回局部变量的引用 | 悬空引用 | optional 存值不存引用 |
// ❌ 危险写法
std::optional<int> foo();
int val = foo().value(); // 如果 foo() 返回空,直接崩溃
// ✅ 安全写法
auto opt = foo();
if (opt) {
int val = *opt;
// 使用 val...
}
// ✅ 更简洁
int val = foo().value_or(0); // 空时用默认值
std::variant 的异常处理
| 操作 | 异常 |
|---|---|
std::get<T>(v) 类型不匹配 |
抛 std::bad_variant_access |
| 赋值新值时构造失败 | 抛构造函数异常,variant 保持原值 |
std::get_if<T>(&v) 类型不匹配 |
返回 nullptr(安全,不抛异常) |
std::variant<int, std::string> v = 42;
// ❌ 类型不对就炸
// auto& s = std::get<std::string>(v); // bad_variant_access!
// ✅ 用 get_if 安全获取
if (auto* p = std::get_if<std::string>(&v)) {
std::cout << *p << std::endl;
} else {
std::cout << "当前不是 string,index = " << v.index() << std::endl;
}
与传统方式对比
optional 替代指针
// 传统方式 —— 容易忘记判空,可能返回悬空指针
User* findUser(int id) {
auto it = userMap.find(id);
return (it != userMap.end()) ? &it->second : nullptr;
}
// C++17 方式 —— 语义清晰,不可能有悬空引用
std::optional<User> findUser(int id) {
auto it = userMap.find(id);
if (it != userMap.end()) {
return it->second; // 拷贝返回,安全
}
return std::nullopt;
}
variant 替代 union
// 传统方式 —— 需手动维护类型标签
struct Value {
enum Type { INT, DOUBLE, STRING } type;
union {
int i;
double d;
const char* s;
};
void setInt(int v) { type = INT; i = v; }
void setDouble(double v) { type = DOUBLE; d = v; }
// 每加一种类型就要改一堆代码...
};
// C++17 方式 —— 类型安全,自动管理
using Value = std::variant<int, double, std::string>;
Value v = 42; // 自动识别类型
v = 3.14; // 安全切换
v = std::string("hello"); // 析构函数自动调用
编译器支持
| 编译器 | 最低版本 | 备注 |
|---|---|---|
| GCC | 7.0+ | 完整支持 |
| Clang | 4.0+ | 完整支持 |
| MSVC | 19.12+ (VS 2017 15.3+) | 完整支持 |
编译命令示例:
# GCC / Clang
g++ -std=c++17 -o main main.cpp
# MSVC (Developer Command Prompt)
cl /std:c++17 main.cpp
如果使用较老的编译器,可以考虑 Boost.Optional 和 Boost.Variant 作为替代方案。
总结
| 特性 | std::optional | std::variant |
|---|---|---|
| 核心用途 | 表达"可能没有值" | 安全存储多种类型 |
| 替代方案 | 指针、特殊标记值 | union + 类型标签 |
| 关键 API | has_value(), value(), value_or() |
index(), get<>(), visit() |
| 异常风险 | 空值访问抛异常 | 类型不匹配抛异常 |
| 编译器要求 | C++17 | C++17 |
核心理念: 让类型系统帮你管理状态,而不是靠程序员的记忆力和自觉性。optional 让"空"有了明确的类型语义,variant 让多类型存储不再危险。
这两个工具配合使用,能大幅减少运行时错误,让代码更安全、更易读。
下一篇预告
下一篇我们将介绍 std::string_view —— 一个轻量级的字符串视图类型。它不拥有字符串数据,却能替代 const std::string& 作为函数参数,在避免不必要拷贝的同时保持接口简洁。如果你经常被字符串拷贝性能困扰,下一篇不容错过!
💬 觉得有帮助? 点赞 + 收藏,下次找得到!有问题欢迎评论区交流~
更多推荐

所有评论(0)