0.1 C++17 关键特性
C++17 是继 C++11 之后改动最大的一个版本,带来了大量实用的语言特性和标准库扩充。本章聚焦三个对日常开发影响最深的特性。
0.1.1 结构化绑定(Structured Binding)
0.1.1.1 它解决了什么问题
在 C++17 之前,从函数返回多个值,或者遍历 map 时,代码写起来非常繁琐:
cpp
// 返回多个值:要么用 pair/tuple,要么用出参
std::pair<int, std::string> get_info() {
return {42, "hello"};
}
auto result = get_info();
int id = result.first; // 靠名字猜含义
std::string name = result.second;
// 遍历 map:类型又长又难看
std::map<std::string, int> scores;
for (const std::pair<const std::string, int>& kv : scores) {
std::cout << kv.first << ": " << kv.second;
}
结构化绑定让这些代码变得直观:
cpp
// C++17:一行拆开,名字有意义
auto [id, name] = get_info();
// 遍历 map
for (const auto& [key, value] : scores) {
std::cout << key << ": " << value;
}
0.1.1.2 基本语法
cpp
auto [变量1, 变量2, ...] = 表达式;
绑定方式有三种,和普通变量声明一样:
cpp
auto [a, b] = expr; // 值绑定,拷贝一份
auto& [a, b] = expr; // 引用绑定,绑定原对象
const auto& [a, b] = expr; // const 引用绑定
0.1.1.3 绑定 pair 和 tuple
cpp
std::pair<int, std::string> p = {1, "alice"};
auto [id, name] = p; // id == 1,name == "alice"
std::tuple<int, double, std::string> t = {42, 3.14, "hello"};
auto [x, y, z] = t; // x == 42,y == 3.14,z == "hello"
0.1.1.4 绑定结构体
结构化绑定可以直接拆开结构体的成员,按声明顺序依次绑定:
cpp
struct Point {
int x;
int y;
int z;
};
Point p{1, 2, 3};
auto [x, y, z] = p; // x==1, y==2, z==3
注意:结构体必须满足"聚合类型"条件------没有用户自定义构造函数、没有私有成员、没有虚函数。否则需要自己实现 get<> 接口(较少用到)。
0.1.1.5 绑定数组
cpp
int arr[3] = {10, 20, 30};
auto [a, b, c] = arr; // a==10, b==20, c==30
0.1.1.6 引用绑定可以修改原数据
cpp
std::map<std::string, int> scores = {{"alice", 90}, {"bob", 80}};
for (auto& [name, score] : scores) {
score += 5; // 直接修改 map 中的值,不是副本
}
值绑定则是拷贝,修改不影响原数据:
cpp
for (auto [name, score] : scores) {
score += 5; // 只修改了副本,scores 不变
}
0.1.1.7 配合函数返回多个值
这是结构化绑定最高频的使用场景之一:
cpp
// 返回操作结果 + 错误信息
std::pair<bool, std::string> try_connect(const std::string& addr) {
if (addr.empty()) return {false, "地址为空"};
// ... 连接逻辑
return {true, ""};
}
auto [ok, err] = try_connect("127.0.0.1");
if (!ok) {
std::cerr << "连接失败: " << err << "\n";
}
cpp
// map::insert 返回 pair<iterator, bool>
std::map<std::string, int> m;
auto [it, inserted] = m.insert({"key", 42});
if (inserted) {
std::cout << "插入成功\n";
}
0.1.1.8 常见陷阱
陷阱1:变量数量必须和成员数量完全匹配
cpp
std::pair<int, int> p = {1, 2};
auto [a] = p; // ❌ 编译错误,数量不匹配
auto [a, b, c] = p; // ❌ 编译错误,数量不匹配
auto [a, b] = p; // ✅
陷阱2:值绑定是拷贝,不是引用
cpp
std::pair<int, int> p = {1, 2};
auto [a, b] = p; // 拷贝
a = 100;
std::cout << p.first; // 仍然是 1,p 没有被修改
陷阱3:auto 推导的是整体类型,不是单个变量
cpp
std::pair<int, double> p = {1, 3.14};
auto [a, b] = p;
// a 是 int,b 是 double
// 并不是 auto a = ...; auto b = ...;
// 而是从整体推导出来的
0.1.1.9 小结
| 绑定方式 | 效果 | 适用场景 |
|---|---|---|
auto [a, b] = expr |
值拷贝 | 只读,不需要修改原数据 |
auto& [a, b] = expr |
引用 | 需要修改原数据 |
const auto& [a, b] = expr |
const 引用 | 只读,且避免拷贝开销 |
遍历容器时推荐 const auto&(避免拷贝),需要修改时用 auto&。
0.1.2 if constexpr ------ 模板神器
0.1.2.1 它解决了什么问题
模板编程中经常需要根据类型走不同的分支,C++17 之前只能用模板特化或 std::enable_if,代码极其难看:
cpp
// C++11 写法:为每种情况写一个特化
template<typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
print_type(T val) {
std::cout << "整数: " << val;
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
print_type(T val) {
std::cout << "浮点: " << val;
}
if constexpr 让这一切变成普通的 if 语句:
cpp
// C++17 写法:一个函数搞定
template<typename T>
void print_type(T val) {
if constexpr (std::is_integral_v<T>) {
std::cout << "整数: " << val;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "浮点: " << val;
} else {
std::cout << "其他: " << val;
}
}
0.1.2.2 和普通 if 的本质区别
这是理解 if constexpr 的关键:
cpp
template<typename T>
void func(T val) {
if constexpr (std::is_integral_v<T>) {
// 只有 T 是整数时,这段代码才会被编译
std::cout << val * 2;
} else {
// 只有 T 不是整数时,这段代码才会被编译
std::cout << val;
}
}
普通 if:两个分支都会被编译,只是运行时选择哪个执行。
if constexpr :条件在编译期 求值,不满足条件的分支根本不编译,就像那段代码不存在一样。
这个区别非常重要:
cpp
template<typename T>
void func(T val) {
if (std::is_integral_v<T>) {
val.some_int_method(); // ❌ T 是 double 时,这行仍然会被编译,报错
}
}
template<typename T>
void func(T val) {
if constexpr (std::is_integral_v<T>) {
val.some_int_method(); // ✅ T 是 double 时,这行根本不被编译
}
}
0.1.2.3 基本语法
cpp
if constexpr (编译期常量表达式) {
// 条件为 true 时编译这里
} else if constexpr (另一个编译期表达式) {
// 条件为 true 时编译这里
} else {
// 其他情况编译这里
}
条件必须是编译期可求值的常量表达式 ,最常见的是 type_traits 中的各种判断。
0.1.2.4 实用示例:统一处理不同类型
cpp
// 根据类型选择不同的序列化方式
template<typename T>
std::string serialize(T val) {
if constexpr (std::is_same_v<T, bool>) {
return val ? "true" : "false";
} else if constexpr (std::is_integral_v<T>) {
return std::to_string(val);
} else if constexpr (std::is_floating_point_v<T>) {
return std::to_string(val);
} else if constexpr (std::is_same_v<T, std::string>) {
return "\"" + val + "\"";
} else {
static_assert(false, "不支持的类型"); // 编译期报错
}
}
serialize(42); // "42"
serialize(true); // "true"
serialize(3.14); // "3.140000"
serialize("hello"s); // "\"hello\""
0.1.2.5 实用示例:递归终止条件
cpp
// 打印任意数量的参数(可变参数模板)
template<typename T>
void print(T val) {
std::cout << val << "\n"; // 只剩一个参数时的终止
}
// C++11 写法:需要两个函数重载
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...); // 递归
}
// C++17 写法:一个函数内用 if constexpr 处理终止条件
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << " ";
print(rest...); // 只有还有剩余参数时才递归,否则这行不编译
}
std::cout << "\n";
}
print(1, 2.0, "hello"); // "1 2 hello"
0.1.2.6 实用示例:根据类型选择成员函数
cpp
struct Cat { void meow() { std::cout << "meow\n"; } };
struct Dog { void bark() { std::cout << "woof\n"; } };
struct Fish { void swim() { std::cout << "splash\n"; } };
template<typename Animal>
void make_sound(Animal a) {
if constexpr (std::is_same_v<Animal, Cat>) {
a.meow(); // 只有 Cat 才编译这行
} else if constexpr (std::is_same_v<Animal, Dog>) {
a.bark(); // 只有 Dog 才编译这行
} else {
a.swim(); // 其他类型编译这行
}
}
make_sound(Cat{}); // "meow"
make_sound(Dog{}); // "woof"
make_sound(Fish{}); // "splash"
用普通 if 这段代码无法编译------Cat 没有 bark() 方法,即使运行时不会走到那个分支,编译器也会报错。
0.1.2.7 配合 static_assert 给出友好的错误信息
cpp
template<typename T>
void process(T val) {
if constexpr (std::is_integral_v<T>) {
// 处理整数
} else if constexpr (std::is_floating_point_v<T>) {
// 处理浮点
} else {
// 不支持的类型:给出清晰的编译错误,而不是一堆模板实例化错误
static_assert(sizeof(T) == 0, "process() 不支持此类型,请传入整数或浮点数");
}
}
process("hello"); // 编译错误:process() 不支持此类型,请传入整数或浮点数
注意:static_assert(false, ...) 在某些编译器下即使分支不被选中也会触发,用 sizeof(T) == 0(永远为 false 但依赖 T)才能正确做到"只有走到这个分支才报错"。
0.1.2.8 常见陷阱
陷阱1:条件必须是编译期常量,不能是运行时变量
cpp
bool flag = true;
if constexpr (flag) { // ❌ flag 是运行时变量,不是编译期常量
// ...
}
constexpr bool cflag = true;
if constexpr (cflag) { // ✅ constexpr 变量可以
// ...
}
陷阱2:if constexpr 只在模板内才有"不编译丢弃分支"的效果
cpp
// 非模板函数中,丢弃的分支仍然需要语法正确
void func() {
if constexpr (false) {
int x = "hello"; // ❌ 仍然报错,语法检查依然进行
}
}
// 模板函数中才能真正跳过不相关的代码
template<typename T>
void func(T val) {
if constexpr (std::is_integral_v<T>) {
val.non_existent_method(); // ✅ T 不是整数时,这行不编译
}
}
0.1.2.9 小结
| 对比项 | 普通 if |
if constexpr |
|---|---|---|
| 条件求值时机 | 运行时 | 编译期 |
| 不满足条件的分支 | 编译但不执行 | 不编译(直接丢弃) |
| 适用场景 | 普通逻辑分支 | 模板中根据类型选择代码路径 |
| 对类型的要求 | 两个分支都必须对当前类型合法 | 只有选中的分支需要合法 |
0.1.3 std::optional ------ 替代返回错误码
0.1.3.1 它解决了什么问题
函数有时候"可能有结果,也可能没有结果",C++17 之前有几种处理方式,但都有缺陷:
cpp
// 方式1:返回特殊值(-1、nullptr、"")表示失败
// 问题:-1 可能是合法值,语义不清晰
int find_user(const std::string& name) {
// 没找到怎么办?返回 -1?但 -1 可能是合法 ID
if (not_found) return -1;
return user_id;
}
// 方式2:输出参数
// 问题:调用侧代码丑,而且必须先声明变量
bool find_user(const std::string& name, int& out_id) {
if (not_found) return false;
out_id = user_id;
return true;
}
int id; // 必须先声明
bool ok = find_user("alice", id);
// 方式3:抛出异常
// 问题:性能开销大,不适合"找不到"这种正常情况
int find_user(const std::string& name) {
if (not_found) throw std::runtime_error("not found");
return user_id;
}
std::optional<T> 提供了语义清晰的方案:要么有值,要么什么都没有。
cpp
std::optional<int> find_user(const std::string& name) {
if (not_found) return std::nullopt; // 明确表示"没有值"
return user_id; // 有值时直接返回
}
0.1.3.2 基本用法
cpp
#include <optional>
// 创建有值的 optional
std::optional<int> a = 42;
std::optional<int> b(42);
auto c = std::make_optional(42);
// 创建空的 optional
std::optional<int> empty;
std::optional<int> empty2 = std::nullopt;
// 检查是否有值
if (a.has_value()) { ... }
if (a) { ... } // 支持隐式转 bool,更常用
// 获取值
int val = a.value(); // 有值时返回值,无值时抛出 std::bad_optional_access
int val = *a; // 解引用,无值时 UB(不检查,要自己确认)
int val = a.value_or(0); // 有值返回值,无值返回默认值 0
0.1.3.3 完整示例:查找操作
cpp
std::optional<std::string> find_config(const std::string& key) {
static std::map<std::string, std::string> config = {
{"host", "localhost"},
{"port", "8080"},
};
auto it = config.find(key);
if (it == config.end()) {
return std::nullopt; // 没找到,返回空
}
return it->second; // 找到了,返回值
}
// 调用侧:语义清晰
if (auto host = find_config("host")) {
std::cout << "host: " << *host << "\n";
} else {
std::cout << "host 未配置\n";
}
// 提供默认值
std::string port = find_config("port").value_or("80");
0.1.3.4 value_or ------ 优雅的默认值处理
cpp
std::optional<int> get_timeout() { return std::nullopt; }
// 旧写法
int timeout;
auto result = get_timeout();
if (result) timeout = *result;
else timeout = 30;
// optional 写法:一行搞定
int timeout = get_timeout().value_or(30);
0.1.3.5 修改和重置
cpp
std::optional<int> opt;
opt = 42; // 赋值,现在有值
opt = std::nullopt; // 重置为空
opt.reset(); // 等价于赋值 nullopt
// emplace:直接在 optional 内部构造,避免拷贝
std::optional<std::string> s;
s.emplace("hello"); // 直接构造,不是先构造再拷贝
0.1.3.6 和指针的对比
optional 和指针有些相似,但有重要区别:
| 对比项 | T* |
std::optional<T> |
|---|---|---|
| 表达"可能没有值" | ✅(nullptr) | ✅(nullopt) |
| 内存分配 | 堆分配(new) | 栈上内联,无堆分配 |
| 所有权语义 | 不明确 | 明确拥有值 |
| 空悬指针风险 | 有 | 无 |
| 与多态配合 | ✅ | ❌(不支持运行时多态) |
cpp
// optional 的值存在对象内部,不需要 new
std::optional<std::string> s = "hello";
// s 的内存全在栈上,没有任何堆分配
0.1.3.7 链式调用(C++23 完善,C++17 手动实现)
cpp
// 多个可能失败的操作串联
std::optional<int> parse_int(const std::string& s) {
try { return std::stoi(s); }
catch (...) { return std::nullopt; }
}
std::optional<int> double_if_positive(int x) {
if (x > 0) return x * 2;
return std::nullopt;
}
// C++17 手动链式
auto result = parse_int("42");
if (result) result = double_if_positive(*result);
if (result) std::cout << *result; // 84
0.1.3.8 在类成员中使用
cpp
struct Config {
std::string host;
int port;
std::optional<std::string> proxy; // 代理是可选的
std::optional<int> timeout; // 超时是可选的
};
Config cfg{"localhost", 8080};
// cfg.proxy 和 cfg.timeout 都是 nullopt
if (cfg.proxy) {
std::cout << "使用代理: " << *cfg.proxy;
}
int timeout = cfg.timeout.value_or(30);
这比用 bool has_proxy + string proxy 两个字段优雅得多,语义上"可选字段"一目了然。
0.1.3.9 常见陷阱
陷阱1:解引用空的 optional 是未定义行为
cpp
std::optional<int> opt;
int x = *opt; // ❌ UB,opt 是空的
int x = opt.value(); // ✅ 抛出 std::bad_optional_access,安全失败
// 正确做法:先检查
if (opt) {
int x = *opt; // ✅ 确认有值后再解引用
}
陷阱2:optional 不适合存放引用
cpp
std::optional<int&> opt; // ❌ 编译错误,optional 不支持引用类型
// 需要的话用指针代替
std::optional<std::reference_wrapper<int>> opt; // 迂回方案,很少用
陷阱3:不要把 optional 用于性能敏感路径
optional 有少量开销(存储一个 bool 标志位,以及可能的内存对齐 padding)。对于极度性能敏感的热路径,考虑直接用特殊值或指针。
陷阱4:不要把 optional 当 bool 用
cpp
std::optional<bool> opt = false;
if (opt) { // 这里是 true!因为 opt 有值(值是 false)
// 这里会执行
}
if (opt.value()) { // 这里才是判断值本身,是 false,不执行
// 这里不执行
}
optional<bool> 中的 if(opt) 判断的是"有没有值",不是"值是不是 true"。
0.1.3.10 小结
| 场景 | 推荐方案 |
|---|---|
| 函数可能找不到结果 | optional<T> 返回值 |
| 类的可选字段 | optional<T> 成员变量 |
| 需要带错误信息的失败 | std::expected<T,E>(C++23)或异常 |
| 多态对象的可选持有 | unique_ptr<T>(可为 nullptr) |
| 性能极度敏感场景 | 裸指针或特殊值(需要仔细评估) |
0.1.4 总结
0.1.4.1 三个特性速查
结构化绑定
cpp
auto [a, b] = pair_or_struct; // 值拷贝
auto& [a, b] = pair_or_struct; // 引用,可修改
const auto& [a, b] = pair_or_struct; // const 引用,推荐用于遍历
核心价值:消灭 .first/.second,让多返回值代码可读。
if constexpr
cpp
template<typename T>
void func(T val) {
if constexpr (std::is_integral_v<T>) {
// 仅对整数类型编译此分支
} else {
// 仅对非整数类型编译此分支
}
}
核心价值:替代 enable_if/模板特化,让模板分支逻辑变成普通 if 语句,不满足条件的分支根本不编译。
std::optional
cpp
std::optional<T> func() {
if (失败) return std::nullopt;
return 结果;
}
auto result = func();
if (result) { use(*result); } // 检查后解引用
auto val = result.value_or(默认值); // 有值取值,没值取默认
核心价值:替代"用特殊值表示失败"或"出参 + bool 返回",用类型系统强制调用方处理"可能没有值"的情况。
0.1.4.2 三个特性的关联
这三个特性在实践中经常搭配使用:
cpp
// 查找配置,返回 key-value 对,或者空
std::optional<std::pair<std::string, int>> find_entry(const std::string& key);
// 调用时:optional 判断是否有值 + 结构化绑定拆开 pair
if (auto entry = find_entry("timeout")) {
auto& [k, v] = *entry;
std::cout << k << " = " << v;
}
// if constexpr 则活跃在模板中,处理不同类型的 optional
template<typename T>
void print_optional(const std::optional<T>& opt) {
if (!opt) {
std::cout << "(empty)";
return;
}
if constexpr (std::is_same_v<T, std::string>) {
std::cout << "\"" << *opt << "\""; // 字符串加引号
} else {
std::cout << *opt;
}
}