📑 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 解构 pair
std::pair<int, std::string> p{42, "hello"};
auto [id, name] = p;

// 解构 tuple
std::tuple<int, double, std::string> t{1, 3.14, "world"};
auto [x, y, z] = t;

// 解构数组
int arr[3] = {1, 2, 3};
auto [a, b, c] = arr;

// 解构结构体(按成员声明顺序)
struct Point {
int x;
int y;
};
Point pt{10, 20};
auto [px, py] = pt;

// 在范围 for 循环中使用
std::map<std::string, int> m{{"a", 1}, {"b", 2}};
for (const auto& [key, value] : m) {
std::cout << key << ": " << value << "\n";
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 传统方式
std::map<int, std::string> m;
auto it = m.find(42);
if (it != m.end()) {
// 使用 it
}
// it 在这里仍可见,可能被误用

// C++17 方式:it 的作用域限定在 if 内
if (auto it = m.find(42); it != m.end()) {
// 使用 it
}

// 配合结构化绑定
std::map<int, std::string> m{{1, "one"}, {2, "two"}};
if (auto [it, inserted] = m.emplace(3, "three"); inserted) {
std::cout << "Inserted: " << it->second << "\n";
}

// switch 初始化语句
if (auto it = m.find(42); it != m.end()) {
switch (it->second.size()) {
case 1: /* 短串 */ break;
case 2: /* 中串 */ break;
default: /* 长串 */ break;
}
}

emplace 返回 pair<iterator, bool>,配合结构化绑定 + if 初始化,插入结果检查一行搞定。这种"初始化 + 条件 + 使用"的紧凑写法是 C++17 惯用法。

constexpr if

if constexpr (cond) 在编译期求值条件,false 分支的代码不会被实例化(甚至不需要语法合法到能编译,只要模板参数替换后落在 false 分支)。这是 C++17 模板元编程的利器,大幅简化了原先依赖 SFINAE/标签派发的分支代码。

它让"根据类型走不同实现"能像普通 if 一样写,但分支在编译期裁剪——运行期零开销,且 false 分支不参与重载决议/实例化。

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
auto getValue(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // 仅当 T 是指针时实例化解引用
} else {
return t;
}
}

int x = 42;
std::cout << getValue(x) << "\n"; // 42
std::cout << getValue(&x) << "\n"; // 42

关键区别于普通 if:普通 if 两个分支都要能编译(只是运行期跳过),if constexpr 的 false 分支直接不实例化。这让"对指针解引用"和"直接返回"能在同一模板里共存——普通 if 会因非指针类型解引用而编译失败。

折叠表达式

折叠表达式把参数包 Args... 用二元运算符折叠成单个值,消除 C++11 递归模板展开参数包的繁琐样板。一个表达式 (args + ...) 替代了"基础 case + 递归 case"两个模板。

四种形式:一元右折叠 (pack op ...)、一元左折叠 (... op pack)、二元右折叠 (pack op ... init)、二元左折叠 (init op ... pack)。空包时二元折叠返回初值,一元折叠只有部分运算符(&&/||/,)有定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 参数包展开
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 右折叠:((1+2)+3)+...
}

// 使用
int total = sum(1, 2, 3, 4, 5); // 15

// 左折叠,常用于流输出
template<typename... Args>
auto printAll(Args... args) {
(std::cout << ... << args) << "\n"; // 二元左折叠,初值为 cout
}

printAll(1, " ", 2, " ", 3); // 1 2 3

// 空参数包:用逗号折叠,空包合法(返回 void)
template<typename... Args>
void log(Args... args) {
((std::cout << args << "\n"), ...); // 一元右折叠
}

sum 用一元右折叠 (args + ...)printAll 用二元左折叠 (cout << ... << args),初值是 cout,这样空包也合法。折叠表达式让"对参数包做归约"成为一行代码,是 C++17 对变参模板的最大改善。

内联变量

C++17 之前,类的 static 成员变量或头文件中的全局变量只能声明在头文件、定义在某个 .cpp 中——否则多翻译单元包含会触发重复定义链接错误。这让"头文件中的常量"很麻烦。inline 变量允许在头文件中定义变量,链接器合并重复定义(像 inline 函数一样)。

这特别适合头文件库(header-only)、static 成员常量、模板相关的全局状态。

1
2
3
4
5
6
7
8
9
10
// 头文件中定义静态成员变量
struct MyClass {
static inline int value = 42; // C++17,无需 .cpp 定义
static inline std::vector<int> data = {1, 2, 3};
};

// 全局变量
inline constexpr int MAX_SIZE = 1000;

// 不再需要在 .cpp 文件中定义

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
2
3
4
5
6
7
8
9
10
11
12
13
14
std::pair p{42, "hello"};           // std::pair<int, const char*>
std::tuple t{1, 2.0, "three"}; // std::tuple<int, double, const char*>

// 推导指引
template<typename T>
struct MyContainer {
MyContainer(T val) : value_(val) {}
T value_;
};

MyContainer c{42}; // MyContainer<int>

// 容器推导
std::vector v{1, 2, 3}; // std::vector<int>

注意 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
2
3
4
5
6
7
8
9
10
11
// C++17
namespace outer::inner {
void func() {}
}

// 等同于 C++17 之前
namespace outer {
namespace inner {
void func() {}
}
}

对深层模块化命名空间(如 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// [[nodiscard]]:忽略返回值会警告
[[nodiscard]] int compute() {
return 42;
}

// compute(); // 警告:返回值被忽略

// [[maybe_unused]]:抑制未使用警告
[[maybe_unused]] int x = 42;

// [[fallthrough]]:显式表示 switch 穿透
switch (value) {
case 1:
doSomething();
[[fallthrough]];
case 2:
doSomethingElse();
break;
}

// [[nodiscard("reason")]] 自定义消息(C++20)
[[nodiscard("Connection must be closed")]]
Connection connect();

[[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
2
3
4
5
6
// UTF-8 字符串字面量(C++17 中类型为 const char[],C++20 起为 const char8_t[])
auto str = u8"Hello, 世界!"; // UTF-8 编码

// Unicode 字符字面量
char16_t c2 = u'汉'; // UTF-16
char32_t c3 = U'中'; // UTF-32

跨平台处理 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <optional>

std::optional<int> divide(int a, int b) {
if (b == 0) {
return std::nullopt; // 表示无值
}
return a / b;
}

auto result = divide(10, 2);
if (result) {
std::cout << *result << "\n"; // 5
}

// 使用 value_or 提供默认值
int value = divide(10, 0).value_or(-1); // -1

// 直接构造
std::optional<std::string> opt("hello");

常用接口:has_value()/operator bool 判是否有值、value() 取值(空则抛 bad_optional_access)、value_or(x) 取值或默认、* 取值(不检查)。optional 是值类型(内联存储 T,可能多一个标志位),适合廉价或中等大小的 TT 很大时考虑 optional<T&>(C++26)或指针。

std::variant

std::variant<Ts...> 是类型安全的联合体:同一时刻持有若干类型之一,且始终知道当前是哪种(带判别式)。它取代了裸 union(不安全、不知当前类型、不调析构)和" tagged struct 指针多态"。

variant 是值语义的多态——不需要基类/虚函数/堆分配,适合"封闭类型集合"(所有可能类型已知)。开放集合(可随时加新类型)仍用继承多态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <variant>

std::variant<int, double, std::string> var;

var = 42;
std::cout << std::get<int>(var) << "\n"; // 42

// 使用 std::visit:对当前持有的类型调用对应重载
auto visitor = [](auto&& arg) {
std::cout << arg << "\n";
};
std::visit(visitor, var);

// 使用 std::holds_alternative 检查类型
if (std::holds_alternative<int>(var)) {
std::cout << "Contains int\n";
}

// 获取索引
std::cout << var.index() << "\n"; // 0

std::visit(visitor, var) 是核心:根据当前类型分派到 visitor 的对应重载(通常配合重载集)。std::get<T> 类型错误时抛 bad_variant_accessstd::get_if<T>(&var) 返回指针(不抛)。variant 默认构造持有第一个类型,要求第一个类型可默认构造。

std::any

std::any 是类型擦除的"任意类型值"容器——能存任何可拷贝类型,运行期通过 typeid 查询实际类型、用 any_cast 取回。相比 variantany 的类型集合是开放的(编译期不限定),代价是动态分配(小对象优化外)、查询靠 typeid

适用场景:配置系统存任意值、插件接口传不透明值、序列化中间表示。能用 variant 就别用 any——variant 编译期已知类型、无堆分配、类型安全更强。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <any>

std::any a = 42;
a = 3.14;
a = std::string("hello");

// 检查类型
if (a.type() == typeid(std::string)) {
std::cout << std::any_cast<std::string>(a) << "\n";
}

// 使用 std::any_cast
try {
int value = std::any_cast<int>(a); // 抛出异常(当前是 string)
} catch (const std::bad_any_cast& e) {
std::cout << "Bad cast: " << e.what() << "\n";
}

any_cast<T>(a) 类型不符时抛 bad_any_castany_cast<T>(&a) 返回指针(不符返回 nullptr)。any 要求值可拷贝构造。小对象通常内联存储(小对象优化),大的堆分配。

std::string_view

std::string_view 是字符串的非拥有视图——只持指针+长度,不分配、不拷贝。它统一了 std::stringconst char*、字符串字面量的只读访问接口,函数参数接受 string_view 时三种实参都能传且无拷贝。

它取代了"函数参数该用 const string& 还是 const char*"的两难——const string&const char* 会临时构造 string(堆分配),const char*string 又丢长度。string_view 两全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <string_view>

// 避免字符串拷贝
void printString(std::string_view sv) {
std::cout << sv << "\n";
}

std::string str = "hello";
const char* cstr = "world";

printString(str); // OK
printString(cstr); // OK
printString("test"); // OK

// 子串操作
std::string_view sv = "Hello, World!";
std::cout << sv.substr(0, 5) << "\n"; // Hello
std::cout << sv.starts_with("Hello") << "\n"; // true(starts_with 是 C++20)

关键陷阱: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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <cstddef>

// 字节类型,用于访问原始内存
std::byte b{42};

std::byte data[4];
data[0] = std::byte{0x12};
data[1] = std::byte{0x34};

// 转换为整数
int value = std::to_integer<int>(data[0]); // 18

// 内存操作
std::memset(data, std::byte{0}, sizeof(data));

适用网络协议、序列化、加密、内存拷贝等"操作原始字节"的场景。用 byte 让接口意图清晰——缓冲区是字节流而非字符串。位运算(|/&/^/<<)直接支持,整型运算需先 to_integer

std::invoke

std::invoke(f, args...) 统一调用任何可调用对象:函数指针、函数引用、仿函数、Lambda、成员函数指针、成员数据指针。它实现了 INVOKE 概念——标准库内部用来定义可调用性的统一规则。

之前调用成员函数指针要 ((obj).*pmf)(args) 这种别扭语法,std::invoke 统一为函数调用形式。在泛型代码里尤其有用——不必区分"是函数还是成员指针"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <functional>

// 调用函数指针
int add(int a, int b) { return a + b; }
int result = std::invoke(add, 3, 4); // 7

// 调用成员函数
struct Calculator {
int multiply(int a, int b) { return a * b; }
};
Calculator calc;
int product = std::invoke(&Calculator::multiply, calc, 3, 4); // 12

// 调用 lambda
auto lambda = [](int a, int b) { return a * b; };
int lambdaResult = std::invoke(lambda, 5, 6); // 30

成员数据指针也支持:std::invoke(&Point::x, pt)pt.xstd::invokestd::functionstd::bind、Ranges 等的基础设施。C++20 的 std::invoke_r 还指定返回类型。

std::apply

std::apply(f, tuple) 把 tuple 的元素展开为 f 的参数调用——即"调用一个以 tuple 为参数包的函数"。它消除了用 integer_sequence 手写展开的样板,是 tuple 与可调用对象之间的桥梁。

1
2
3
4
5
6
7
8
9
10
11
#include <tuple>

int add(int a, int b, int c) { return a + b + c; }

auto t = std::make_tuple(1, 2, 3);
int result = std::apply(add, t); // 6,等价于 add(1, 2, 3)

// 访问 tuple 元素
std::apply([](auto&... args) {
((std::cout << args << "\n"), ...);
}, t);

配合泛型 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
2
3
4
5
6
7
8
9
10
#include <tuple>

struct Point {
int x;
int y;
int z;
};

auto t = std::make_tuple(1, 2, 3);
Point p = std::make_from_tuple<Point>(t); // Point{1, 2, 3}

适用"构造参数被打包成 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
2
3
4
5
6
7
8
9
10
11
12
#include <algorithm>

int value = 75;
int minVal = 0;
int maxVal = 100;

int clamped = std::clamp(value, minVal, maxVal); // 75
int clamped2 = std::clamp(-10, minVal, maxVal); // 0
int clamped3 = std::clamp(150, minVal, maxVal); // 100

// 带比较函数
auto clamped4 = std::clamp(value, minVal, maxVal, std::greater<int>());

要求 lo <= hi,否则行为未定义(C++20 才加强检查)。可自定义比较函数(如 greater 实现"反向"区间)。常用于 UI 坐标、传感器读数、数值规整。

std::reduce/std::transform_reduce

std::reducestd::accumulate 的并行友好版:不要求左结合(可任意结合顺序),因此能并行执行。std::transform_reduce 先对每个元素做变换再归约,合并了 transform + reduce 两步,避免中间容器。

两者都支持执行策略(std::execution::par),可并行化。这是 C++17 并行算法的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <numeric>
#include <execution>

std::vector<int> vec = {1, 2, 3, 4, 5};

// 并行归约
int sum = std::reduce(std::execution::par, vec.begin(), vec.end());

// 先转换后归约:平方和
auto squaredSum = std::transform_reduce(
std::execution::par,
vec.begin(), vec.end(),
0LL,
std::plus<>{},
[](int x) { return x * x; }
); // 55 (1+4+9+16+25)

reduceaccumulate 的区别: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <filesystem>

namespace fs = std::filesystem;

// 创建目录
fs::create_directory("test");
fs::create_directories("a/b/c"); // 递归创建

// 遍历目录
for (const auto& entry : fs::directory_iterator(".")) {
std::cout << entry.path() << "\n";
std::cout << "Is file: " << entry.is_regular_file() << "\n";
}

// 检查文件
if (fs::exists("file.txt")) {
std::cout << "File size: " << fs::file_size("file.txt") << "\n";
}

// 路径操作
fs::path p = "/home/user/documents/file.txt";
std::cout << "Filename: " << p.filename() << "\n";
std::cout << "Extension: " << p.extension() << "\n";
std::cout << "Parent: " << p.parent_path() << "\n";

// 相对路径
std::cout << "Relative: " << fs::relative(p) << "\n";

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
2
3
4
5
6
7
8
9
10
11
12
13
#include <mutex>

std::mutex mtx1, mtx2;

// C++17 之前:手动 lock + adopt
std::lock(mtx1, mtx2);
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);

// C++17:scoped_lock 自动管理多个锁,完全替代 std::lock_guard
std::scoped_lock lock(mtx1, mtx2);
// 自动以避免死锁的方式获取所有锁
// 析构时自动释放

单个互斥量时 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
2
3
4
5
6
7
8
9
10
#if __has_include(<optional>)
#include <optional>
#endif

#if __has_include(<filesystem>) && __has_include(<version>)
#include <filesystem>
#define HAS_FILESYSTEM 1
#else
#define HAS_FILESYSTEM 0
#endif

<version> 是 C++20 引入的版本信息头,存在与否常用于判断库是否完整支持新标准。__has_include 配合 __cpp_* 特性宏,能让同一份代码在不同标准/实现下自适应,是过渡期库的必备工具。

并行算法

C++17 给 <algorithm> 的多数算法加了"执行策略"重载,支持并行/向量化执行。传 std::execution::par 即可让 sort/find/transform 等并行运行,标准库自动分块调度。

三种策略:seq(串行,等同传统)、par(多线程并行)、par_unseq(并行+向量化,允许无序交织,要求操作无数据竞争且可向量化)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <algorithm>
#include <execution>

std::vector<int> vec(1000000);
std::iota(vec.begin(), vec.end(), 0);

// 执行策略
std::sort(std::execution::par, vec.begin(), vec.end()); // 并行
std::sort(std::execution::seq, vec.begin(), vec.end()); // 顺序
std::sort(std::execution::par_unseq, vec.begin(), vec.end()); // 并行+向量化

// 并行查找
auto it = std::find(std::execution::par, vec.begin(), vec.end(), 42);

// 并行变换
std::transform(std::execution::par, vec.begin(), vec.end(), vec.begin(),
[](int x) { return x * 2; });

注意:并行算法要求操作可安全并发调用(函数对象无状态或线程安全),且部分算法(如 par_unseq)禁止在回调中用互斥锁/分配等"向量化不友好"操作。实现可能依赖 TBB 等后端,需链接。数据量小时代价可能反而高于串行。

std::shared_mutex

std::shared_mutex 是 C++17 的读写锁(不带超时,轻量于 C++14 的 shared_timed_mutex)。支持共享锁(多读并发)和独占锁(互斥写)。读多写少的场景下提升并发度。

读用 std::shared_lock、写用 std::unique_lock。读写锁比普通 mutex 开销大,读操作占主导且较重时才划算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <shared_mutex>

class ThreadSafeCache {
mutable std::shared_mutex mtx_;
std::unordered_map<std::string, std::string> cache_;

public:
// 读操作:使用共享锁,多线程可并发读
std::string read(const std::string& key) const {
std::shared_lock<std::shared_mutex> lock(mtx_);
auto it = cache_.find(key);
return it != cache_.end() ? it->second : "";
}

// 写操作:使用独占锁,阻塞所有读
void write(const std::string& key, const std::string& value) {
std::unique_lock<std::shared_mutex> lock(mtx_);
cache_[key] = value;
}
};

注意 mtx_mutable——readconst 方法但仍需锁。写者会阻塞所有读者(独占),读者多时写可能饿死,必要时考虑写优先策略。读操作很短或写频繁时,普通 mutex 往往更快——读写锁的额外状态维护有成本。

常用特性总结

最常用的 C++17 特性:

  • 结构化绑定
  • std::optional
  • std::variant
  • std::string_view
  • std::invoke
  • if 初始化语句
  • std::filesystem
  • [[nodiscard]] 属性