C++17 - 重大更新
📑 C++ 演进系列:总览 | C++11 | C++14 | C++17 | C++20 | C++23 | C++26
C++17 带来了许多重要的新特性,显著提升了 C++ 的表达力和安全性。它虽不如 C++11/C++20 那般颠覆,但落地了大量"早就该有"的实用特性:结构化绑定、optional/variant/any/string_view、文件系统、并行算法,以及一批语法糖(if 初始化、折叠表达式、CTAD)。C++17 是当前工程实践的主力标准,多数代码库已普遍可用。
结构化绑定
结构化绑定把一个聚合对象"拆开"成多个具名变量,一步到位。它消除了 pair/tuple 访问时满屏 std::get<0>/.first 的啰嗦,也让 map 遍历不再需要 it->first/it->second。
支持三种对象:tuple-like(pair/tuple/array)、数组、结构体(按声明顺序绑定公有非静态成员)。绑定形式有 auto [a,b](值拷贝)、auto& [a,b](引用)、const auto&(只读引用)。
1 | |
map 遍历是最常见的受益场景:for (const auto& [key, value] : m) 一行替代了迭代器解引用。注意结构体绑定要求成员是公有的且不能有静态成员混入;绑定的变量名按成员声明顺序对应,名字随意但顺序固定。
if 和 switch 初始化语句
if (init; cond) 和 switch (init; cond) 允许在条件前加一条初始化语句,其作用域限定在 if/switch 及其分支内。它解决了"为条件准备一个临时变量,结果变量泄漏到外层作用域"的问题——既污染命名空间,又可能被误用。
最典型场景:if (auto it = m.find(key); it != m.end()),迭代器 it 只在 if 内有效,出 if 即销毁,干净利落。
1 | |
emplace 返回 pair<iterator, bool>,配合结构化绑定 + if 初始化,插入结果检查一行搞定。这种"初始化 + 条件 + 使用"的紧凑写法是 C++17 惯用法。
constexpr if
if constexpr (cond) 在编译期求值条件,false 分支的代码不会被实例化(甚至不需要语法合法到能编译,只要模板参数替换后落在 false 分支)。这是 C++17 模板元编程的利器,大幅简化了原先依赖 SFINAE/标签派发的分支代码。
它让"根据类型走不同实现"能像普通 if 一样写,但分支在编译期裁剪——运行期零开销,且 false 分支不参与重载决议/实例化。
1 | |
关键区别于普通 if:普通 if 两个分支都要能编译(只是运行期跳过),if constexpr 的 false 分支直接不实例化。这让"对指针解引用"和"直接返回"能在同一模板里共存——普通 if 会因非指针类型解引用而编译失败。
折叠表达式
折叠表达式把参数包 Args... 用二元运算符折叠成单个值,消除 C++11 递归模板展开参数包的繁琐样板。一个表达式 (args + ...) 替代了"基础 case + 递归 case"两个模板。
四种形式:一元右折叠 (pack op ...)、一元左折叠 (... op pack)、二元右折叠 (pack op ... init)、二元左折叠 (init op ... pack)。空包时二元折叠返回初值,一元折叠只有部分运算符(&&/||/,)有定义。
1 | |
sum 用一元右折叠 (args + ...)。printAll 用二元左折叠 (cout << ... << args),初值是 cout,这样空包也合法。折叠表达式让"对参数包做归约"成为一行代码,是 C++17 对变参模板的最大改善。
内联变量
C++17 之前,类的 static 成员变量或头文件中的全局变量只能声明在头文件、定义在某个 .cpp 中——否则多翻译单元包含会触发重复定义链接错误。这让"头文件中的常量"很麻烦。inline 变量允许在头文件中定义变量,链接器合并重复定义(像 inline 函数一样)。
这特别适合头文件库(header-only)、static 成员常量、模板相关的全局状态。
1 | |
static inline 成员变量在类内直接定义,省去了 .cpp 中的 int MyClass::value = 42;。inline constexpr 全局常量可放头文件被多 TU 包含而不报错。这让大量"配置常量"能干净地放头文件,促成 header-only 库。
类模板参数推导(CTAD)
CTAD 让编译器从构造函数参数推导模板参数,省去手写模板参数列表。std::pair p{42, "x"} 自动推导出 std::pair<int, const char*>,不必写 std::pair<int, const char*>。
推导可由构造函数签名隐式发生,也可由"推导指引"(deduction guide)显式定制——后者用于调整默认推导行为。标准库为常用容器提供了推导指引。
1 | |
注意 std::vector v{1, 2, 3} 推导出 vector<int>,但 std::vector v(3, 0) 推导出 vector<int>(3 个 0),而 std::vector v{3, 0} 也推导成 vector<int> 含两个元素 3 和 0——花括号优先匹配 initializer_list,与构造函数语义交互需留意。CTAD 对简单构造很方便,对歧义场景仍建议显式写参数。
嵌套命名空间
namespace a::b::c {} 一行声明多层嵌套命名空间,等价于三层 namespace 嵌套。这是个纯粹的语法糖,消除此前层层缩进的样板。
1 | |
对深层模块化命名空间(如 project::module::detail)尤其省事。注意 C++17 不支持 inline namespace a::b(C++20 才支持组合形式),需 namespace a { inline namespace b {} }。
属性改进
C++17 扩展了属性体系,新增三个常用属性并允许给 [[nodiscard]] 附消息:
[[nodiscard]]:返回值不应被忽略,忽略则警告。用于错误码、资源句柄、const访问器——防止lock()返回的锁被丢弃这种隐蔽 bug。[[maybe_unused]]:抑制"未使用"警告。用于条件编译可能不用的变量、保留接口中暂未用的参数。[[fallthrough]]:显式声明switch穿透是有意的,抑制-Wimplicit-fallthrough警告。
1 | |
[[nodiscard]] 加消息是 C++20 特性(原文标注需注意)。[[fallthrough]] 必须单独成语句、放在穿透发生处。[[maybe_unused]] 也可用于函数/类/枚举。
UTF-8 字符字面量
C++17 完善了 Unicode 字面量的类型体系:u8 前缀表示 UTF-8。注意在 C++17 中 u8"..." 的类型仍是 const char[](C++20 才引入 char8_t 并改为 const char8_t[]),但明确了其 UTF-8 编码语义。
u/U 前缀分别对应 UTF-16/UTF-32 字符与字符串。
1 | |
跨平台处理 Unicode 时,u8 字面量保证源码中的字符串以 UTF-8 存储,不受编译器执行字符集影响。C++20 的 char8_t 让 UTF-8 字符串有独立类型,能与普通 char 区分,但也在与期望 const char* 的旧 API 交互时引入摩擦。
std::optional
std::optional<T> 表示"可能有值也可能没有"——要么持有一个 T,要么空(nullopt)。它类型安全地表达"无值",取代了"用特殊哨兵值表示失败"(如 -1 表示无效、空字符串表示缺失)和"用指针/bool 出参"的旧模式。
适合"失败不携带错误详情"的场景:查找可能未命中、配置可能未设置、解析可能为空。若失败需要原因,用 C++23 的 std::expected。
1 | |
常用接口:has_value()/operator bool 判是否有值、value() 取值(空则抛 bad_optional_access)、value_or(x) 取值或默认、* 取值(不检查)。optional 是值类型(内联存储 T,可能多一个标志位),适合廉价或中等大小的 T;T 很大时考虑 optional<T&>(C++26)或指针。
std::variant
std::variant<Ts...> 是类型安全的联合体:同一时刻持有若干类型之一,且始终知道当前是哪种(带判别式)。它取代了裸 union(不安全、不知当前类型、不调析构)和" tagged struct 指针多态"。
variant 是值语义的多态——不需要基类/虚函数/堆分配,适合"封闭类型集合"(所有可能类型已知)。开放集合(可随时加新类型)仍用继承多态。
1 | |
std::visit(visitor, var) 是核心:根据当前类型分派到 visitor 的对应重载(通常配合重载集)。std::get<T> 类型错误时抛 bad_variant_access,std::get_if<T>(&var) 返回指针(不抛)。variant 默认构造持有第一个类型,要求第一个类型可默认构造。
std::any
std::any 是类型擦除的"任意类型值"容器——能存任何可拷贝类型,运行期通过 typeid 查询实际类型、用 any_cast 取回。相比 variant,any 的类型集合是开放的(编译期不限定),代价是动态分配(小对象优化外)、查询靠 typeid。
适用场景:配置系统存任意值、插件接口传不透明值、序列化中间表示。能用 variant 就别用 any——variant 编译期已知类型、无堆分配、类型安全更强。
1 | |
any_cast<T>(a) 类型不符时抛 bad_any_cast,any_cast<T>(&a) 返回指针(不符返回 nullptr)。any 要求值可拷贝构造。小对象通常内联存储(小对象优化),大的堆分配。
std::string_view
std::string_view 是字符串的非拥有视图——只持指针+长度,不分配、不拷贝。它统一了 std::string、const char*、字符串字面量的只读访问接口,函数参数接受 string_view 时三种实参都能传且无拷贝。
它取代了"函数参数该用 const string& 还是 const char*"的两难——const string& 接 const char* 会临时构造 string(堆分配),const char* 接 string 又丢长度。string_view 两全。
1 | |
关键陷阱:string_view 不拥有数据,生命周期依赖被引用字符串。从临时 string 构造 string_view 并存起来,临时销毁后视图悬挂。substr 返回的还是 string_view(O(1) 切片,不拷贝)。不要用 string_view 接收需要长期持有的数据。starts_with/ends_with/contains 是 C++20/23 才加的成员。
std::byte
std::byte 是表示原始字节的类型,本质是 enum class byte : unsigned char。它表达"这是一坨原始比特,不是字符也不是数字"的语义,避免用 char(字符语义)或 unsigned char(数字语义)表示内存字节时的歧义。
只能通过 to_integer<T> 转出为整数、用位运算操作,不能隐式转整型——强制开发者明确"我在把字节当数字用"。
1 | |
适用网络协议、序列化、加密、内存拷贝等"操作原始字节"的场景。用 byte 让接口意图清晰——缓冲区是字节流而非字符串。位运算(|/&/^/<<)直接支持,整型运算需先 to_integer。
std::invoke
std::invoke(f, args...) 统一调用任何可调用对象:函数指针、函数引用、仿函数、Lambda、成员函数指针、成员数据指针。它实现了 INVOKE 概念——标准库内部用来定义可调用性的统一规则。
之前调用成员函数指针要 ((obj).*pmf)(args) 这种别扭语法,std::invoke 统一为函数调用形式。在泛型代码里尤其有用——不必区分"是函数还是成员指针"。
1 | |
成员数据指针也支持:std::invoke(&Point::x, pt) 取 pt.x。std::invoke 是 std::function、std::bind、Ranges 等的基础设施。C++20 的 std::invoke_r 还指定返回类型。
std::apply
std::apply(f, tuple) 把 tuple 的元素展开为 f 的参数调用——即"调用一个以 tuple 为参数包的函数"。它消除了用 integer_sequence 手写展开的样板,是 tuple 与可调用对象之间的桥梁。
1 | |
配合泛型 Lambda([](auto&... args))能遍历 tuple 所有元素——这是 C++17 之前要写一堆模板才能做到的。apply 要求 tuple 大小与函数参数数匹配。
std::make_from_tuple
std::make_from_tuple<T>(tuple) 用 tuple 的元素作为构造参数构造 T 对象——即"从 tuple 构造"。与 std::apply 的区别:apply 调用现成可调用对象,make_from_tuple 调用 T 的构造函数。
1 | |
适用"构造参数被打包成 tuple 传递"的场景,如从序列化数据/数据库行构造对象、工厂模式中参数聚合后分发。make_from_tuple 保证 T 可在 tuple 元素按顺序构造时成立。
std::clamp
std::clamp(v, lo, hi) 把 v 限制在 [lo, hi] 区间内——小于 lo 返回 lo、大于 hi 返回 hi、否则返回 v。它替代了 std::max(std::min(v, hi), lo) 这种易写错顺序的写法。
1 | |
要求 lo <= hi,否则行为未定义(C++20 才加强检查)。可自定义比较函数(如 greater 实现"反向"区间)。常用于 UI 坐标、传感器读数、数值规整。
std::reduce/std::transform_reduce
std::reduce 是 std::accumulate 的并行友好版:不要求左结合(可任意结合顺序),因此能并行执行。std::transform_reduce 先对每个元素做变换再归约,合并了 transform + reduce 两步,避免中间容器。
两者都支持执行策略(std::execution::par),可并行化。这是 C++17 并行算法的一部分。
1 | |
reduce 与 accumulate 的区别:accumulate 严格左结合、串行;reduce 结合顺序任意、可并行,但要求归约运算可结合可交换(否则结果可能与 accumulate 不同)。transform_reduce 还支持双范围版本(如内积)。初值类型决定累加类型——用 0LL 避免大数溢出。
std::filesystem
std::filesystem 是跨平台文件系统库(源自 Boost.Filesystem),统一了路径操作、目录遍历、文件查询/复制/删除等。此前各平台 API 不同(POSIX dirent vs Win32 FindFirstFile),跨平台文件代码很痛苦。filesystem 用一套类型安全的接口抹平差异。
核心类型:path(路径,跨平台)、directory_entry/directory_iterator/recursive_directory_iterator(遍历)、file_status(权限/类型)。函数:exists/file_size/create_directory/copy/remove 等。
1 | |
path 的 / 运算符拼接路径("a" / "b.txt"),跨平台自动用正确分隔符。遍历时 recursive_directory_iterator 递归子目录。文件操作可能抛 filesystem_error,也可传 error_code 重载避免异常。注意 file_size 对不存在文件的行为。
std::scoped_lock
std::scoped_lock 是 C++17 对多锁获取的统一方案:一次构造锁定任意数量的互斥量,并用避免死锁的算法(类似 std::lock 的 try-and-back-off)获取。析构时全部释放。它完全取代了 std::lock_guard——单个锁时两者等价,多锁时 scoped_lock 是唯一选择。
死锁是多锁的经典坑:线程 A 先锁 m1 再锁 m2、线程 B 先锁 m2 再锁 m1,互相等待。scoped_lock 用一致的获取顺序/回退算法避免。
1 | |
单个互斥量时 std::scoped_lock lock(mtx) 与 std::lock_guard<std::mutex> lock(mtx) 等价,CTAD 省去模板参数。scoped_lock 是 C++17 起的默认选择,lock_guard 仅为兼容保留。
__has_include
__has_include(<header>) 是预处理期特性,判断某头文件是否存在。它让代码能根据标准库/编译器支持情况条件包含——在 C++17/20 过渡期写跨版本代码很有用。
1 | |
<version> 是 C++20 引入的版本信息头,存在与否常用于判断库是否完整支持新标准。__has_include 配合 __cpp_* 特性宏,能让同一份代码在不同标准/实现下自适应,是过渡期库的必备工具。
并行算法
C++17 给 <algorithm> 的多数算法加了"执行策略"重载,支持并行/向量化执行。传 std::execution::par 即可让 sort/find/transform 等并行运行,标准库自动分块调度。
三种策略:seq(串行,等同传统)、par(多线程并行)、par_unseq(并行+向量化,允许无序交织,要求操作无数据竞争且可向量化)。
1 | |
注意:并行算法要求操作可安全并发调用(函数对象无状态或线程安全),且部分算法(如 par_unseq)禁止在回调中用互斥锁/分配等"向量化不友好"操作。实现可能依赖 TBB 等后端,需链接。数据量小时代价可能反而高于串行。
std::shared_mutex
std::shared_mutex 是 C++17 的读写锁(不带超时,轻量于 C++14 的 shared_timed_mutex)。支持共享锁(多读并发)和独占锁(互斥写)。读多写少的场景下提升并发度。
读用 std::shared_lock、写用 std::unique_lock。读写锁比普通 mutex 开销大,读操作占主导且较重时才划算。
1 | |
注意 mtx_ 标 mutable——read 是 const 方法但仍需锁。写者会阻塞所有读者(独占),读者多时写可能饿死,必要时考虑写优先策略。读操作很短或写频繁时,普通 mutex 往往更快——读写锁的额外状态维护有成本。
常用特性总结
最常用的 C++17 特性:
- 结构化绑定
std::optionalstd::variantstd::string_viewstd::invoke- if 初始化语句
std::filesystem[[nodiscard]]属性








