第24章 C++14特性
第24章 C++14特性
如果说C++11是C++的文艺复兴,那C++14就是这场运动的"配套设施完善年"。功能还是那些功能,但用起来更顺手了,就像是你家楼下终于开了家便利店——你不需要再跑五公里买泡面了。本章我们就来聊聊C++14那些让人会心一笑的小改进。
24.1 泛型Lambda
什么是Lambda?
在聊泛型Lambda之前,先给Lambda(匿名函数)下个定义。顾名思义,Lambda就是没有名字的函数——就像你点了份外卖,不需要知道厨师叫什么,只需要知道味道好不好吃就行了。
Lambda的基本语法是这样的:
1
| [capture](parameters) -> return_type { body }
|
其中capture是捕获列表(决定你能访问哪些外部变量),parameters是参数,return_type是返回类型,body是函数体。
C++11 vs C++14:参数类型的进化
在C++11中,Lambda的参数必须是明确指定的类型,比如:
1
2
3
4
| // C++11风格的Lambda,参数类型必须写死
auto add = [](int a, int b) {
return a + b;
};
|
这样就只能做整数加法了,想做浮点数加法?对不起,再写一个。
但到了C++14,参数类型可以用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
26
27
28
29
| #include <iostream>
#include <vector>
#include <algorithm>
int main() {
// C++14: Lambda参数可以使用auto
// 编译器会根据传入的参数自动推断a和b的类型
auto add = [](auto a, auto b) {
return a + b;
};
// 整数加法
std::cout << "add(1, 2) = " << add(1, 2) << std::endl; // 输出: 3
// 浮点数加法
std::cout << "add(1.5, 2.5) = " << add(1.5, 2.5) << std::endl; // 输出: 4
// 字符串拼接(重载了+运算符)
std::cout << "add(std::string(\"a\"), std::string(\"b\")) = "
<< add(std::string("a"), std::string("b")) << std::endl; // 输出: ab
// 甚至可以用在STL算法中!
std::vector<int> nums = {1, 2, 3, 4, 5};
std::for_each(nums.begin(), nums.end(), [](auto& n) {
n *= 2; // 每个元素翻倍
});
return 0;
}
|
工作原理
auto在Lambda参数中的原理其实很朴素:编译器会为每种不同的类型生成一份专门的函数实例。这叫做模板实例化——想象一下,编译器是个超级复制粘贴机器人,你写一个auto参数,它会根据实际调用生成N个版本。
💡 为什么不用模板函数?
直接写模板函数当然也行,但Lambda更简洁,而且可以就地定义、就地使用,不用先跑到别处声明一个模板函数再回来调用。
实际应用场景
泛型Lambda在STL算法中使用频率极高,比如排序自定义、查找过滤、批量转换等等:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<double> prices = {99.9, 199.9, 299.9, 399.9, 499.9};
// 泛型Lambda配合for_each打折
double discount = 0.8; // 八折
std::for_each(prices.begin(), prices.end(), [discount](auto& price) {
price *= discount;
});
// 打印打折后的价格
for (const auto& price : prices) {
std::cout << price << " "; // 输出: 79.92 159.92 239.92 319.92 399.92
}
std::cout << std::endl;
return 0;
}
|
幽默一刻
想象你走进一家餐厅:
C++11的服务员:“您好,请问您要点什么?”
你:“来份加法。”
服务员:“好的,请问是整数加法还是浮点数加法?”
你:"…我只是想算1+1。"
C++14的服务员:“您好,请问您要点什么?”
你:“来份加法,随便什么类型都行。”
服务员:“好的,1+1=2,您的外卖已准备好!”
泛型Lambda就是那个能读懂你心思的服务员。
24.2 变量模板
模板的进化:从函数到变量
C++的模板最初是为了实现泛型编程,但很长一段时间里,模板只能用在函数和类上。到了C++14,模板家族迎来了新成员——变量模板(Variable Template)。
简单来说,变量模板就是用模板定义的变量。就像类模板生成类、函数模板生成函数一样,变量模板生成一系列相关的变量。
语法很简单
1
2
| template<typename T>
constexpr 类型 变量名 = 表达式;
|
std::is_integral_v 的前世今生
有一个经典的需求:判断一个类型是否是整数类型。在C++11里,你只能这么写:
1
2
3
| // C++11 风格:类型萃取类模板 + ::value
std::is_integral<int>::value // 返回true
std::is_integral<double>::value // 返回false
|
这玩意儿又臭又长,每次写都要敲半天。于是人们很自然地想:能不能把它变成一个变量?
1
2
| // C++17 才有的标准写法
std::is_integral_v<int> // 更简洁,效果一样
|
⚠️ 一个极易搞混的时间点(很多教程都写错):
- 变量模板这个语言特性是 C++14 引入的;
- 但标准库里那一大批
_v 后缀(is_integral_v、is_floating_point_v、is_same_v…)
是 C++17 才补上的(提案 P0006R0)。
换句话说:C++14 里根本没有 std::is_integral_v。你要么写 std::is_integral<int>::value,
要么自己定义一个变量模板——而这正是 C++14 让这件事变简单的原因。下面就是完整的例子:
这个_v后缀后来成了标准库的约定。而在 C++14 里,你完全可以自己写一个同样好用的变量模板:
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
28
29
30
31
32
33
34
35
36
| #include <iostream>
#include <cstddef>
#include <type_traits>
// C++14: 变量模板
// 语法:template<typename T> constexpr 类型 变量名 = 初始值;
// 判断T是否是整数类型,结果是一个编译期常量bool
// (注意:这是在"自己造一个 C++17 才有的 std::is_integral_v",所以故意起了同名,
// 用来说明"C++14 里这个得你自己写"。真实项目里请直接写 std::is_integral_v)
template<typename T>
constexpr bool is_integral_v = std::is_integral<T>::value;
// 获取T类型的大小(字节数),结果是一个编译期常量size_t
template<typename T>
constexpr std::size_t type_size_v = sizeof(T);
int main() {
std::cout << std::boolalpha; // 输出true/false而不是1/0
// 判断各种类型是否是整数
std::cout << "is_integral_v<int> = " << is_integral_v<int> << std::endl; // 输出: true
std::cout << "is_integral_v<double> = " << is_integral_v<double> << std::endl; // 输出: false
std::cout << "is_integral_v<char> = " << is_integral_v<char> << std::endl; // 输出: true (char也是整数类型)
std::cout << "is_integral_v<bool> = " << is_integral_v<bool> << std::endl; // 输出: true
std::cout << std::endl;
// 获取各种类型的大小
std::cout << "type_size_v<int> = " << type_size_v<int> << std::endl; // 输出: 4
std::cout << "type_size_v<double> = " << type_size_v<double> << std::endl; // 输出: 8
std::cout << "type_size_v<char> = " << type_size_v<char> << std::endl; // 输出: 1
std::cout << "type_size_v<long long> = " << type_size_v<long long> << std::endl; // 输出: 8
return 0;
}
|
背后的原理
变量模板的本质是:每当你用不同的类型参数去访问这个变量时,编译器会生成一个对应类型的专门版本:
1
2
3
4
5
6
7
8
| // 你写了:
template<typename T>
constexpr bool is_integral_v = std::is_integral<T>::value;
// 编译器生成类似这样的东西(伪代码):
constexpr bool is_integral_v_int = std::is_integral<int>::value; // true
constexpr bool is_integral_v_double = std::is_integral<double>::value; // false
constexpr bool is_integral_v_char = std::is_integral<char>::value; // true
|
常见标准库变量模板
C++17 起,标准库补齐了大量 _v 变量模板(C++14 里一个都没有,得自己写):
| 变量模板 | 判断的类型 |
|---|
is_integral_v<T> | T是否是整数类型 |
is_floating_point_v<T> | T是否是浮点类型 |
is_array_v<T> | T是否是数组类型 |
is_pointer_v<T> | T是否是指针类型 |
is_enum_v<T> | T是否是枚举类型 |
📌 顺带认识两个同类后缀(都是命名约定,不是不同的语言特性):
| 后缀 | 含义 | 新写法 | 老写法 |
|---|
_v | 变量模板,取值 | std::is_integral_v<int> | std::is_integral<int>::value |
_t | 别名模板,取类型 | std::remove_reference_t<int&> | std::remove_reference<int&>::type |
这两个后缀不是同一批进来的,正好可以用来检验你有没有记住本节的重点:
_t(取类型,如 std::remove_const_t<T>)是 C++14 就有的(N3655);_v(取值,如 std::is_integral_v<T>)要等到 C++17(P0006R0)。
实测一下就很清楚:clang++ -std=c++14 编译 std::remove_const_t<const int> 通过,
编译 std::is_integral_v<int> 则报 no template named 'is_integral_v'。
幽默一刻
变量模板的故事是个典型的"起了个大早,赶了个晚集":
- C++11:只有
std::is_integral<int>::value,又臭又长。 - C++14:语言层面给了你变量模板这个工具,但标准库一个都没用上——
你得自己写
template<typename T> constexpr bool is_integral_v = std::is_integral<T>::value;。 - C++17:标准库终于把
_v 版本补齐了(P0006R0),从此 std::is_integral_v<int> 开箱即用。
所以你记住这条容易记反的结论:变量模板是 C++14 的,std::xxx_v 是 C++17 的。
24.3 放宽的constexpr限制
constexpr:从"编译期计算"到"更自由的编译期计算"
constexpr是C++11引入的关键字,意思是"常量表达式",告诉编译器"这个值可以在编译期算出来"。C++11对constexpr的限制非常严格——它只允许很简单的单行表达式,不许有局部变量,不许有循环,不许有多条语句。
这就好比你去相亲,对象说:“我可以和你一起吃饭,但只允许用筷子夹一粒米,不许嚼,不许咽,不许说话。”
到了C++14,这些限制被大幅放宽了。
C++11 vs C++14:constexpr的限制对比
| 特性 | C++11 constexpr | C++14 constexpr |
|---|
| 局部变量 | ❌ 不允许 | ✅ 允许 |
| 局部静态变量 | ❌ 不允许 | ❌ 不允许 |
| 循环语句 | ❌ 不允许 | ✅ 允许 |
| if语句 | ❌ 不允许(只能用三元运算符) | ✅ 允许 |
| 多个return | ❌ 不允许 | ✅ 允许 |
| try/catch | ❌ 不允许 | ❌ 不允许 |
阶乘和斐波那契:经典constexpr案例
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| #include <iostream>
// C++14: constexpr函数限制放宽
// 可以在constexpr函数中使用:
// - 局部变量
// - 循环语句
// - 改变量语句(虽然这个例子没用到)
// - if-else分支
// 计算阶乘:5! = 5*4*3*2*1 = 120
constexpr int factorial(int n) {
int result = 1; // 局部变量,C++11不允许!
for (int i = 2; i <= n; ++i) { // 循环,C++11不允许!
result *= i;
}
return result;
}
// 计算斐波那契数列第n项
constexpr int fibonacci(int n) {
// C++14允许使用if分支了!
if (n <= 1) return n;
int a = 0, b = 1; // 局部变量
for (int i = 2; i <= n; ++i) { // 循环
int c = a + b;
a = b;
b = c;
}
return b;
}
int main() {
// constexpr变量:编译期就计算出结果
constexpr int fact5 = factorial(5); // 编译期计算,结果是120
constexpr int fib10 = fibonacci(10); // 编译期计算,结果是55
// 普通变量:运行时计算
int runtime_fact = factorial(6); // 运行时计算
std::cout << "factorial(5) = " << fact5 << std::endl; // 输出: 120
std::cout << "fibonacci(10) = " << fib10 << std::endl; // 输出: 55
std::cout << "factorial(6) (runtime) = " << runtime_fact << std::endl; // 输出: 720
return 0;
}
|
编译期计算的优势
为什么要在编译期计算?因为这样可以:
- 提升性能:结果直接嵌入二进制,不需要每次运行都算一遍
- 用于数组大小:编译期常量可以作为数组维度
- 用于模板参数:模板参数必须是编译期常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <iostream>
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
int main() {
// 编译期常量:可以作为数组维度
constexpr int SIZE = factorial(5); // SIZE = 120
int arr[SIZE] = {0}; // 合法!数组大小必须是编译期常量
std::cout << "数组大小是编译期计算的: " << SIZE << std::endl; // 输出: 120
std::cout << "数组arr的元素个数: " << (sizeof(arr) / sizeof(arr[0])) << std::endl; // 输出: 120
return 0;
}
|
幽默一刻
C++11的constexpr就像是一个完美的石膏像——漂亮但一动不动。你想让它循环?不行。想让它分支判断?不行。想让它存个中间结果?更不行。
C++14的constexpr终于允许你"动"了,但仍然有些事情做不到——比如try/catch(编译期异常处理太复杂)、比如动态内存分配。所以constexpr函数里不能new一个对象出来——这大概就是"戴着镣铐跳舞"吧。
24.4 二进制字面量
什么是字面量(Literal)?
字面量就是代码里直接写出来的值,比如42、3.14、"hello"。它们就像是语言的基本词汇——你不需要定义,直接使用。
在C++14之前,我们有十进制字面量(42)、十六进制字面量(0x2A)、八进制字面量(052),但就是没有二进制字面量。
二进制字面量:程序员的"0和1"情怀
程序员喜欢说"一切都是0和1",但C++11之前你却没法直接在代码里写二进制数。你得手动算:0b1010在C++11之前是不存在的,你得写10(十进制)或者0xA(十六进制)。
这就好比一个中世纪铁匠说"我天天和火打交道",但却不让他直接点火,必须用火柴慢慢磨。
C++14终于带来了0b前缀,让你直接写二进制:
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
28
29
30
31
32
33
34
35
36
37
| #include <iostream>
int main() {
// C++14: 二进制字面量
// 语法:0b + 二进制数字串(或0B也行,大小火鸡均可)
int bin1 = 0b1010; // 二进制1010 = 十进制10
int bin2 = 0b11111111; // 二进制11111111 = 十进制255
std::cout << "0b1010 = " << bin1 << std::endl; // 输出: 10
std::cout << "0b11111111 = " << bin2 << std::endl; // 输出: 255
// 也可以用大写B
int bin3 = 0B1010; // 效果一样
// 和位运算组合使用——这是二进制字面量的主战场!
int flags = 0b1100 | 0b0011; // 按位或 = 0b1111 = 15
std::cout << "0b1100 | 0b0011 = " << flags << std::endl; // 输出: 15
// 权限控制示例
const int READ = 0b0001; // 读权限
const int WRITE = 0b0010; // 写权限
const int EXECUTE = 0b0100; // 执行权限
const int ADMIN = 0b1000; // 管理员权限
int user_permissions = READ | WRITE; // 普通用户:读+写
int admin_permissions = READ | WRITE | EXECUTE | ADMIN; // 管理员:全部
// 检查权限
bool can_read = (user_permissions & READ) != 0; // true
bool can_execute = (user_permissions & EXECUTE) != 0; // false
std::cout << "普通用户能否读: " << std::boolalpha << can_read << std::endl; // 输出: true
std::cout << "普通用户能否执行: " << can_execute << std::endl; // 输出: false
return 0;
}
|
典型应用场景
二进制字面量在底层编程、位掩码、权限控制、硬件寄存器操作等场景非常有用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <iostream>
// 网络协议中的标志位
struct TCPFlags {
static constexpr int FIN = 0b00000001;
static constexpr int SYN = 0b00000010;
static constexpr int RST = 0b00000100;
static constexpr int PSH = 0b00001000;
static constexpr int ACK = 0b00010000;
static constexpr int URG = 0b00100000;
};
int main() {
// 构建一个SYN-ACK包
int packet = TCPFlags::SYN | TCPFlags::ACK;
std::cout << "TCP包标志位: " << std::endl;
// 注意:没打开 boolalpha,所以 bool 会打印成 1 / 0
std::cout << " SYN: " << ((packet & TCPFlags::SYN) != 0) << std::endl; // 输出: 1(即 true)
std::cout << " ACK: " << ((packet & TCPFlags::ACK) != 0) << std::endl; // 输出: 1(即 true)
std::cout << " FIN: " << ((packet & TCPFlags::FIN) != 0) << std::endl; // 输出: 0(即 false)
return 0;
}
|
幽默一刻
二进制字面量的命名之争也很有意思。2008年有人提议用0b前缀,遭到了C语言委员会的反对,他们坚持要用B开头的写法——比如B1010。理由是"不能和十六进制的0x太像"。
最后C++14还是选择了0b(和0x保持队形)。现在看来这是个正确的决定——统一的美才是真的美。
24.5 数字分隔符
顾名思义的特性
数字分隔符(Digit Separator)允许在数字字面量中插入单引号,让长数字更容易阅读。1'000'000就是100万,比1000000好看多了。
这个特性的灵感来自Pascal语言(用点作为分隔符)和Fortran语言(用空格),但C++选择了单引号——大概是因为程序员对单引号最熟悉。
语法规则
- 单引号只能放在两个数字之间,并且只能放一个(
1'000 ✅,1''000 ❌) - 不能放在数字的开头或结尾(
1'000' ❌;写成 '1000' 就更糟——那会变成"多字符字面量",见下文) - 不能紧挨着进制前缀(
0x'FF ❌、0b'1010 ❌) - 十六进制里的字母也算"数字",所以
0xFF'00'00 合法;小数点在两边有数字时也能用(1'000.000'1 ✅)
🧪 这是"实测才能记住"的一条。下面几种写法在 Apple clang 21 上的真实结果(别背规则,亲手试一遍):
1
2
3
4
5
6
| int a = 1''000; // ❌ error: expected ';' after top level declarator
// (另有一条 warning: empty character constant)
int b = 0x'FF; // ❌ error: invalid suffix 'x'FF' on integer constant
int c = 1'000'; // ❌ error: expected ';' after top level declarator
// (另有一条 warning: missing terminating ' character)
int e = 0b'1010; // ❌ error: invalid digit 'b' in octal constant
|
⚠️ 最阴的一个坑在这里:int d = '1000';(单引号开头、单引号结尾、里面塞了 4 位数字)
既不报错也不报警告——因为它是合法的多字符字面量(multi-character literal),值是实现定义的。
在 Apple clang 21 上打印出来是 825241648,而标准只保证"它是某个 int"。
看到这种数字,基本可以断定作者本来想写 1'000。
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
28
29
30
31
32
33
34
| #include <iostream>
#include <cstdint>
int main() {
// C++14: 数字分隔符(单引号)
// 目的:让长数字更易读,完全不影响数值!
// 十进制:千位分隔
int million = 1'000'000; // 一百万
int credit = 4'999'999; // 信用卡号?不,这只是五百万减一
int population = 1'400'000'000; // 十四亿
// 二进制:每4位一组(符合人类的阅读习惯)
int mask = 0b1111'0000'1111'0000; // 高低四位都是1
// 十六进制:每2位一组
int color = 0xFF'00'00; // 纯红色(RGB 格式:红 FF、绿 00、蓝 00)
// long long:天文数字也能readably
long long big = 9'223'372'036'854'775'807LL; // LLONG_MAX
std::cout << "million = " << million << std::endl; // 输出: 1000000
std::cout << "credit = " << credit << std::endl; // 输出: 4999999
std::cout << "population = " << population << std::endl; // 输出: 1400000000
std::cout << std::hex; // 切换到十六进制输出
std::cout << "mask = 0x" << mask << std::endl; // 输出: mask = 0xf0f0
std::cout << "color = 0x" << color << std::endl; // 输出: color = 0xff0000
std::cout << std::dec; // 切回十进制
std::cout << "big = " << big << std::endl; // 输出: 9223372036854775807
return 0;
}
|
实际应用
数字分隔符在以下场景特别有用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <iostream>
#include <cstddef> // std::size_t
int main() {
// 物理/数学常数
const double AVOGADRO = 6.022'140'76e23; // 阿伏伽德罗常数
const double PLANCK = 6.626'070'15e-34; // 普朗克常数
// 金额(会计领域)
long double price = 1'999'999.99; // 将近两百万
long double tax = 199'999.90; // 约二十万税
// 字节数
const std::size_t KB = 1'024; // 1KB = 1024字节
const std::size_t MB = 1'048'576; // 1MB = 1024*1024
const std::size_t GB = 1'073'741'824; // 1GB = 1024^3
const std::size_t TB = 1'099'511'627'776; // 1TB(64 位上没问题,32 位会溢出)
std::cout << "1KB = " << KB << " bytes" << std::endl; // 输出: 1KB = 1024 bytes
std::cout << "1MB = " << MB << " bytes" << std::endl; // 输出: 1MB = 1048576 bytes
std::cout << "1GB = " << GB << " bytes" << std::endl; // 输出: 1GB = 1073741824 bytes
return 0;
}
|
幽默一刻
数字分隔符的灵感来源其实很有趣:
- Pascal:用点(
1.000.000)——但这和浮点数小数点冲突了 - Ada:用下划线(
1_000_000)——C++也曾考虑过这个 - Fortran:用空格(
1 000 000)——但空格在代码里经常被忽略 - 最终C++:用单引号(
1'000'000)
为什么选单引号?因为它是键盘上唯一"不属于数字、运算符或标识符"的字符。所以当你写int x = 0b1010'0101;时,编译器可以明确知道分隔符只是用来提升可读性的,不会影响实际数值。
24.6 返回类型推导
什么是返回类型推导?
返回类型推导(Return Type Deduction)允许编译器自动推断函数的返回类型,你不需要显式写出返回值类型。这就像是点菜时说"来个招牌菜",服务员会根据你的口味推荐具体是哪道。
语法
1
2
3
| auto 函数名(参数) { return 表达式; }
// 或者
decltype(auto) 函数名(参数) { return 表达式; }
|
auto vs decltype(auto)
这两者有什么区别?
| 形式 | 行为 |
|---|
auto | 推导返回值类型,会丢掉引用和const |
decltype(auto) | 推导返回值类型,保持引用和const |
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
28
| #include <iostream>
// C++14: 函数返回类型推导
// 使用auto作为返回类型,编译器会自动推断
auto add(int a, int b) {
return a + b; // 编译器推导返回类型为int
}
// 使用decltype(auto)保持表达式的类型语义
decltype(auto) multiply(int a, int b) {
return a * b; // 保持表达式类型,这里是int
}
// 引用类型演示
int x = 10;
auto get_x() { return x; } // 返回int,不是int&
// decltype(auto) get_x_ref() { return x; } // 返回int&
int main() {
std::cout << "add(3, 4) = " << add(3, 4) << std::endl; // 输出: 7
std::cout << "multiply(5, 6) = " << multiply(5, 6) << std::endl; // 输出: 30
// 验证返回类型
std::cout << "sizeof(add(3,4)) = " << sizeof(add(3, 4)) << std::endl; // 输出: 4 (int的大小)
return 0;
}
|
decltype规则简介
decltype会根据表达式推导类型,规则如下:
- 如果表达式是标识符或类成员访问,返回该对象的类型
- 如果表达式是函数调用,返回该函数的声明返回类型
- 其他情况,返回该表达式的类型
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
28
29
| #include <iostream>
#include <type_traits>
int global_var = 42;
int& get_ref() { return global_var; }
const int const_val = 100;
int main() {
// decltype(auto)的行为演示
// 返回int(标识符规则)
decltype(auto) a = global_var; // int
// 返回int&(引用规则)
decltype(auto) b = get_ref(); // int&
// 返回const int(常量规则)
decltype(auto) c = const_val; // const int
std::cout << std::boolalpha;
std::cout << "is_same<decltype(a), int>: "
<< std::is_same<decltype(a), int>::value << std::endl; // 输出: true
std::cout << "is_same<decltype(b), int&>: "
<< std::is_same<decltype(b), int&>::value << std::endl; // 输出: true
std::cout << "is_same<decltype(c), const int>: "
<< std::is_same<decltype(c), const int>::value << std::endl; // 输出: true
return 0;
}
|
限制和注意事项
返回类型推导不是万能的:
- 前向声明问题:如果你声明了返回类型推导的函数,在定义之前调用它,编译器可能无法正确处理
- 多返回值的函数:如果函数有多个return语句且返回类型不一致,编译器会报错
- 虚函数:虚函数不能使用返回类型推导
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #include <iostream>
// 错误示例:多个返回类型不一致
// auto wrong(int flag) {
// if (flag > 0) return 1; // int
// else return 2.0; // double - 冲突!
// }
// 正确示例:多个return但类型一致
auto absolute(int n) {
if (n >= 0) return n; // 返回int
else return -n; // 返回int,类型一致
}
int main() {
std::cout << "absolute(-5) = " << absolute(-5) << std::endl; // 输出: 5
return 0;
}
|
幽默一刻
返回类型推导有一个著名的"陷阱":
1
| auto make_int() { return 1; }
|
你觉得返回类型是什么?int?恭喜你,答对了。
但如果是:
1
2
| const int& foo();
auto bar() { return foo(); }
|
bar()的返回类型是什么?int?还是const int&?
答案是int——因为auto会丢掉引用和const修饰符。如果你想要保留const int&,得用decltype(auto)。
所以记住:auto是"差不多就行",decltype(auto)是"原汁原味"。
24.7 std::make_unique
智能指针的进化史
在C++11之前,管理动态内存是一项技术活——new和delete配对使用,稍有不慎就会内存泄漏。到了C++11,标准库引入了智能指针,让内存管理变得自动化。
C++11有std::shared_ptr(共享所有权)和std::unique_ptr(独占所有权),但有个问题:std::make_shared早就有了,std::make_unique却迟迟没有加入标准库。
这就好比餐厅提供打包盒(shared),但不提供外卖袋(unique)——你得自己找袋子装。
C++14终于补上了这个漏洞。
std::make_unique是什么?
std::make_unique是一个辅助函数,用于创建std::unique_ptr对象。它比直接使用new构造更安全、更简洁。
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
28
29
30
31
32
33
34
35
| #include <iostream>
#include <memory>
// 定义一个Widget类来演示构造和析构
struct Widget {
Widget() { std::cout << "Widget constructed" << std::endl; }
~Widget() { std::cout << "Widget destructed" << std::endl; }
};
int main() {
// C++14: std::make_unique(之前只有make_shared)
// 语法:std::make_unique<T>(构造参数...)
// 创建单个对象
auto up1 = std::make_unique<int>(42);
std::cout << "Value: " << *up1 << std::endl; // 输出: 42
// 创建数组(C++14支持数组形式的make_unique)
auto upArr = std::make_unique<int[]>(5);
upArr[0] = 10;
upArr[1] = 20;
std::cout << "upArr[0] = " << upArr[0] << std::endl; // 输出: 10
std::cout << "upArr[1] = " << upArr[1] << std::endl; // 输出: 20
// 异常安全演示
// make_unique在构造前分配内存,更安全
// 如果构造过程中抛出异常,已分配的内存会被自动释放
auto w = std::make_unique<Widget>(); // 会打印 "Widget constructed"
std::cout << "Widget正在发挥作用..." << std::endl;
// 函数结束时,w会自动销毁,打印 "Widget destructed"
return 0;
}
|
为什么推荐使用make_unique?
- 异常安全:在"一行里干好几件事"的表达式里,裸
new 可能在你还没把指针交给智能指针时就抛异常,内存就丢了 - 不会泄漏:手动
new/delete 一旦提前 return 或抛异常,delete 就永远执行不到;unique_ptr 的析构总会被调用 - 代码简洁:不需要重复写两遍类型名(
std::unique_ptr<T>(new T(...)) vs std::make_unique<T>(...))
❌ 一个常见的错误说法:有些教程写"make_unique 可以一次性分配对象和引用计数块,所以更快"。
这是把 make_shared 的特性套到了 make_unique 头上——unique_ptr 根本没有引用计数块,
也就不存在"合并分配"这回事。make_unique 的快,主要体现在少写一遍类型名、少一次手写 new,
真正的价值是异常安全,不是性能。
先看最直观的一条差异——“提前返回"时会发生什么:
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
28
29
30
31
32
33
34
| #include <iostream>
#include <memory>
struct Resource {
Resource() { std::cout << "Resource acquired" << std::endl; }
~Resource() { std::cout << "Resource released" << std::endl; }
};
// ① 手动 new/delete:一旦提前返回(或抛异常),delete 就永远执行不到 → 泄漏
void with_raw_pointer(bool earlyExit) {
Resource* r = new Resource();
if (earlyExit) {
return; // ⚠️ 泄漏!r 指向的内存没人释放
}
delete r;
}
// ② 智能指针:不管怎么离开作用域,析构函数都会跑
void with_unique_ptr(bool earlyExit) {
auto r = std::make_unique<Resource>();
if (earlyExit) {
return; // ✅ 依然会释放
}
}
int main() {
std::cout << "=== 裸指针版本(提前返回 → 泄漏)===" << std::endl;
with_raw_pointer(true); // 只看到 acquired,看不到 released
std::cout << "=== unique_ptr 版本(提前返回 → 仍然释放)===" << std::endl;
with_unique_ptr(true); // acquired 和 released 都会出现
return 0;
}
|
输出:
1
2
3
4
5
| === 裸指针版本(提前返回 → 泄漏)===
Resource acquired
=== unique_ptr 版本(提前返回 → 仍然释放)===
Resource acquired
Resource released
|
还有一条更隐蔽的异常安全问题,也是 make_unique 存在的最大理由:当它出现在"一个函数调用的实参列表"里时,
手写 new 有可能在两个实参求值之间抛异常,导致已经分配的内存无人认领:
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
28
29
30
| #include <iostream>
#include <memory>
#include <stdexcept>
struct Widget {
Widget() { std::cout << "Widget 构造" << std::endl; }
~Widget() { std::cout << "Widget 析构" << std::endl; }
};
int mayThrow() {
throw std::runtime_error("出错了");
}
void use(std::unique_ptr<Widget> w, int n) { (void)w; (void)n; }
int main() {
try {
// ❌ 危险写法(注释掉,跑起来会泄漏):
// new Widget() 先把裸指针造好,接着给 use 求第二个实参时抛异常,
// 那个裸指针就没人负责了。
// use(std::unique_ptr<Widget>(new Widget), mayThrow());
// ✅ 安全写法:make_unique 返回时所有权已经在 unique_ptr 里,
// 之后求值即使抛异常,临时对象也会把 Widget 释放掉。
use(std::make_unique<Widget>(), mayThrow());
} catch (const std::exception& e) {
std::cout << "捕获异常: " << e.what() << std::endl;
}
return 0;
}
|
输出(注意:Widget 构造 之后紧跟着 Widget 析构,说明异常路径上资源也没丢):
1
2
3
| Widget 构造
Widget 析构
捕获异常: 出错了
|
实际使用场景
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
28
29
30
31
32
33
34
35
| #include <iostream>
#include <memory>
#include <vector>
class Node {
public:
int value;
std::unique_ptr<Node> next;
Node(int v) : value(v), next(nullptr) {
std::cout << "Node(" << value << ") created" << std::endl;
}
~Node() {
std::cout << "Node(" << value << ") destroyed" << std::endl;
}
};
int main() {
// 创建链表
auto head = std::make_unique<Node>(1);
head->next = std::make_unique<Node>(2);
head->next->next = std::make_unique<Node>(3);
// 遍历链表
std::cout << "链表内容: ";
for (auto* cur = head.get(); cur != nullptr; cur = cur->next.get()) {
std::cout << cur->value << " ";
}
std::cout << std::endl;
// unique_ptr超出作用域后自动释放所有节点
return 0;
}
|
幽默一刻
上一版这里写了个段子,说"有人问 Bjarne Stroustrup 为什么,他说:呃……我们忘了?"——
这个引语是编的,Stroustrup 没说过这话,标准委员会的会议记录里也没有"我们忘了一件事"这种承认。
真实的情况平淡得多:make_shared 在 C++11 就进了标准(它和 shared_ptr 一起设计),
而 make_unique 的提案(N3588,作者 Stephan T. Lavavej)提交得太晚,赶不上 C++11 的特性冻结,
只能排到 C++14。于是就有了这么个尴尬的十年:标准库教你别写 new,
却偏偏没给你 make_unique,大家只好在项目里自己写一个 make_unique 垫着用。
📝 顺带一提:这也解释了为什么很多老代码库里都有一个自己实现的 make_unique——
它们是在 C++14 之前写的。如果你在旧代码里看到自己实现的 make_unique,
而且项目已经升级到 C++14 以上,那就可以把自定义版本删掉、直接用 std::make_unique 了。
24.8 带初始化捕获的Lambda
什么是Lambda捕获?
Lambda表达式可以"捕获"外部变量,使其在Lambda内部可见。但C++11的捕获方式有限:
[=]:按值捕获所有变量[&]:按引用捕获所有变量[x]:按值捕获x[&x]:按引用捕获x
问题来了:**如果我想捕获一个表达式结果,而不是变量本身呢?**比如捕获一个std::unique_ptr的移动后的状态。
C++11的困境:移动捕获的缺失
在C++11中,Lambda不支持移动捕获(也没有 std::make_unique——它是C++14的)。
如果你想把一个"只能移动"的对象(如 std::unique_ptr)弄进 Lambda,是做不到的:
1
2
3
4
5
6
7
| // C++11 的写法:只能移动的对象根本进不了捕获列表
std::unique_ptr<int> up(new int(42)); // C++11 只能用 new 来造
// auto f = [up]() { return *up; }; // ❌ 按值捕获要拷贝,unique_ptr 不可拷贝
// auto f = [&up]() { return *up; }; // ⚠️ 能编译,但只是在引用外面的 up,
// // 一旦 up 离开作用域就成了悬垂引用
// auto f = [up = std::move(up)]() {...}; // ❌ 初始化捕获是 C++14 才有的语法
|
换句话说:C++11 里想把 unique_ptr 安全地搬进 Lambda,无解。
你只能退而求其次——捕获裸指针(丢掉了所有权语义),或者干脆不用 Lambda。
C++14:初始化捕获
C++14引入了初始化捕获(Generalized Lambda Captures),允许你用任意表达式初始化捕获值:
1
| [identifier = expression] (params) { body }
|
这解决了两个问题:
- 移动捕获:将即将销毁的对象移动到Lambda中
- 重命名捕获:给捕获的变量起个新名字
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| #include <iostream>
#include <memory>
#include <vector>
#include <algorithm>
#include <utility>
int main() {
// C++14: Lambda初始化捕获(移动捕获)
// 语法:[捕获名 = 表达式]
// 创建unique_ptr
auto up = std::make_unique<int>(42);
// 初始化捕获:将std::move(up)的结果捕获为value
// up的所有权被转移给Lambda内部的value
auto captured = [value = std::move(up)]() {
return *value; // 现在unique_ptr在Lambda内部,up变成nullptr
};
std::cout << "captured() = " << captured() << std::endl; // 输出: 42
// 更复杂的例子:同时获取最小值和最大值
std::vector<int> data = {1, 2, 3, 4, 5};
// 使用初始化捕获移动data到Lambda中
auto result = [data = std::move(data)]() {
auto minmax = std::minmax_element(data.begin(), data.end());
return std::pair<int, int>(*minmax.first, *minmax.second);
};
// C++14写法:用auto接收pair,再分别提取
auto result_pair = result();
int minVal = result_pair.first;
int maxVal = result_pair.second;
std::cout << "min=" << minVal << ", max=" << maxVal << std::endl; // 输出: min=1, max=5
// data现在已经被移动走了
std::cout << "data的大小: " << data.size() << std::endl; // 输出: 0
return 0;
}
|
初始化捕获的工作原理
初始化捕获其实是两个步骤的简写:
- 创建临时变量:用表达式的结果初始化一个变量
- 捕获该变量:按值或按引用捕获这个新变量
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
28
29
30
31
32
33
34
| #include <iostream>
#include <memory>
#include <string>
int main() {
// [x = expr] 相当于:
// 1. 用expr初始化一个隐藏的变量
// 2. 按值捕获这个变量
// 例子:捕获计算结果
auto compute_and_capture = [result = 1 + 2 + 3]() {
return result; // 返回6
};
std::cout << "计算结果: " << compute_and_capture() << std::endl; // 输出: 6
// 例子:捕获字符串的一部分
std::string full_name = "John Doe";
// "John Doe" 的下标:J0 o1 h2 n3 (空格)4 D5 o6 e7
// substr(5) 从下标 5 开始,得到的是 "Doe"(不包含空格)
auto last_name = [name = full_name.substr(5)]() {
return name;
};
std::cout << "姓氏: '" << last_name() << "'" << std::endl; // 输出: 'Doe'
// 例子:按引用初始化捕获
int counter = 0;
auto increment = [&cnt = counter]() {
++cnt; // 修改外部的counter
};
increment();
std::cout << "计数器: " << counter << std::endl; // 输出: 1
return 0;
}
|
实际应用场景
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| #include <iostream>
#include <memory>
#include <functional>
class Database {
public:
int query(const char* sql) {
std::cout << "执行SQL: " << sql << std::endl;
return 42;
}
};
int main() {
// 场景1:绑定移动后的对象
auto conn = std::make_unique<Database>();
// 移动conn到Lambda中,外部的conn变为空
auto query_func = [db = std::move(conn)](const char* sql) {
return db->query(sql);
};
// 场景2:创建带状态的回调
int invocation_count = 0;
auto counting_callback = [count = 0]() mutable {
++count; // mutable 允许修改"按值捕获"出来的那份副本
return count;
};
std::cout << "第1次调用: " << counting_callback() << std::endl; // 输出: 1
std::cout << "第2次调用: " << counting_callback() << std::endl; // 输出: 2
std::cout << "第3次调用: " << counting_callback() << std::endl; // 输出: 3
// 关键点:count 是 Lambda 对象自己的一个数据成员,从 0 初始化之后就一直在那儿。
// 正是因为有 mutable,才能每次调用都改它,于是依次得到 1、2、3——
// 这恰恰说明它**不是**每次新建,而是在同一个 Lambda 对象里累加。
//
// 至于外部的 invocation_count,它和这个 count 是两个毫无关系的变量,
// 所以调用三次之后,invocation_count 仍然是 0。
std::cout << "外部 invocation_count = " << invocation_count << std::endl; // 输出: 0
return 0;
}
|
幽默一刻
初始化捕获的语法刚出来的时候,很多人吐槽:“这不就是把赋值语句塞进中括号里吗?”
没错,确实是这样。但就是这么一个简单的语法扩展,解决了C++元编程中的一大痛点。
想象一下没有初始化捕获的世界:你有一个unique_ptr,想把它移动到Lambda里,怎么办?你得先std::move到一个局部变量,然后捕获那个变量。但问题是——那个局部变量本身就是个累赘。
有了初始化捕获,一行搞定:[ptr = std::move(original_ptr)]()。
24.9 容易被漏掉的几个 C++14 特性
C++14 的"八大特性"是各大教程的常驻嘉宾,但标准里其实还塞了几个不显眼但很好用的东西。
它们不像泛型 Lambda 那样天天上头条,却经常在你需要的时候突然出现。
24.9.1 [[deprecated]]:正式的标准属性(注意:这是 C++14,不是 C++11!)
[[...]] 这种属性语法是 C++11 引入的,但 C++11 里只定义了 [[noreturn]] 和 [[carries_dependency]]。
大家最常用的 [[deprecated]] 其实是 C++14 才进入标准的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <iostream>
[[deprecated("请改用 newAPI()")]] // 可以带一句解释,编译警告里会原样打出来
void oldAPI() {
std::cout << "老接口" << std::endl;
}
struct [[deprecated]] OldStruct { // 属性写在 struct 关键字之后
int x;
};
int main() {
// oldAPI(); // ⚠️ 编译警告:'oldAPI' is deprecated: 请改用 newAPI()
// OldStruct s; // ⚠️ 编译警告:'OldStruct' is deprecated
std::cout << "已标记废弃的接口,调用时会收到编译警告" << std::endl;
return 0;
}
|
⚠️ 一个实测小坑:Apple clang 21 在 -std=c++11 下也接受 [[deprecated]]
并且照样报 -Wdeprecated-declarations 警告——这是编译器的"向前兼容”,不代表它是 C++11 的特性。
换个严格遵守标准的编译器(或看特性宏 __has_cpp_attribute(deprecated)),就能看出差别。
24.9.2 std::exchange:一步完成"取值 + 换值"
这是 <utility> 里的小工具,签名是 exchange(obj, new_value)——返回 obj 的旧值,同时把 obj 改成 new_value。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <iostream>
#include <string>
#include <utility>
int main() {
std::string a = "hello";
// 把 a 换成 "world",同时把旧值 "hello" 拿走
std::string b = std::exchange(a, "world");
std::cout << "a = " << a << std::endl; // 输出: a = world
std::cout << "b = " << b << std::endl; // 输出: b = hello
return 0;
}
|
它在实现移动构造函数、移动赋值运算符时特别顺手:
1
2
3
4
5
6
7
8
9
| class Buffer {
int* data_ = nullptr;
std::size_t size_ = 0;
public:
Buffer(Buffer&& other) noexcept
: data_(std::exchange(other.data_, nullptr)), // 一步:接管指针 + 把对方置空
size_(std::exchange(other.size_, 0)) {}
};
|
24.9.3 std::integer_sequence:编译期的整数序列
std::integer_sequence<T, Is...> 和它的别名 std::index_sequence<0,1,2,...> 是 C++14 放进 <utility> 的。
它的价值在于:把"参数包"变成"一串可以用在编译期的下标",这是展开 tuple、实现泛型 apply 的基础工具。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <cstddef>
#include <iostream>
#include <utility>
template<typename T, T... Is>
void print_seq(std::integer_sequence<T, Is...>) {
// C++14 里还没有折叠表达式,用数组初始化的老技巧把包展开
int dummy[] = { (std::cout << Is << ' ', 0)... };
(void)dummy;
std::cout << std::endl;
}
int main() {
print_seq(std::integer_sequence<int, 0, 1, 2, 3>{}); // 手写序列
print_seq(std::make_index_sequence<5>{}); // 自动生成 0 1 2 3 4
std::cout << "index_sequence<0,1,2,3,4>::size() = "
<< std::index_sequence<0, 1, 2, 3, 4>{}.size() << std::endl;
return 0;
}
|
输出:
1
2
3
| 0 1 2 3
0 1 2 3 4
index_sequence<0,1,2,3,4>::size() = 5
|
24.9.4 带大小的释放函数(sized deallocation)
这可能是 C++14 里最"存在感为零"的一个特性。C++14 允许你写这样一个释放函数:
1
| static void operator delete(void* p, std::size_t n) noexcept;
|
多了个 n 参数,表示"这块内存有多大"。为什么有用? 因为分配器以前必须把"每块内存多大"记在旁边,
才能在你 delete 的时候还回去;现在编译器可以直接把大小告诉你,分配器就省下了那份记录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <cstddef>
#include <iostream>
#include <new>
struct Big {
double payload[2];
static void* operator new(std::size_t n) {
std::cout << "new: 申请 " << n << " 字节" << std::endl;
return ::operator new(n);
}
// C++14 新增:带 size 参数的释放函数
static void operator delete(void* p, std::size_t n) noexcept {
std::cout << "sized delete: 归还 " << n << " 字节" << std::endl;
::operator delete(p);
}
};
int main() {
Big* b = new Big;
delete b; // 对象大小在编译期已知,于是走"带大小"的版本
return 0;
}
|
输出:
1
2
| new: 申请 16 字节
sized delete: 归还 16 字节
|
💡 注意:上面两个 std::size_t 的值都是 16,因为 Big 里有两个 double——不是 sizeof(Big) 之外的什么东西,
就是对象本身的大小(new 申请的就是它)。这也顺便解释了为什么数组 new[] 需要额外记录元素个数、
而这里的单对象 new 不需要。
24.9.5 std::quoted:让字符串能"带着引号"来回走
<iomanip> 里的 std::quoted。它的用途很具体:输出时给字符串加引号,输入时把引号去掉,
这样含空格的字符串也能安全地读写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string s = "hello world";
std::cout << std::quoted(s) << std::endl; // 输出: "hello world"
std::cout << "长度 = " << s.size() << std::endl; // 输出: 长度 = 11
std::stringstream ss;
ss << std::quoted(s); // 写进去,带引号
std::string back;
ss >> std::quoted(back); // 读出来,引号自动剥掉
std::cout << "读回来 = " << back << std::endl; // 输出: 读回来 = hello world
return 0;
}
|
🎯 为什么需要它:std::cin >> str 遇到空格就停,所以直接把 "hello world" 写出去再读回来,
只会得到 "hello"。std::quoted 用一对引号把内容"包起来",读的时候再拆掉,
于是含空格的字符串就能原样往返。写简单的数据文件、做日志的字段级解析时很好用。
本章小结
C++14是C++11之后的一次重要补丁更新,它不是在开创新的范式,而是在完善已有的功能。本章我们学习了C++14的核心特性:
24.1 泛型Lambda
- 核心变化:
auto可以用于Lambda参数 - 解决的问题:同一个Lambda可以处理不同类型的参数
- 应用场景:STL算法、回调函数、泛型编程
24.2 变量模板
- 核心变化:模板可以用于定义变量
- 解决的问题:给"编译期的值"一个正式的写法,写起来像变量而不是像类型
- ⚠️ 最易记反的一点:变量模板是 C++14;标准库里成批的
std::xxx_v 是 C++17(_t 别名则是 C++14) - 应用场景:编译期类型查询、类型大小判断
24.3 放宽的constexpr限制
- 核心变化:constexpr函数中可以使用局部变量和循环
- 解决的问题:更强大的编译期计算能力
- 应用场景:编译期数学计算、查表优化
24.4 二进制字面量
- 核心变化:
0b前缀可以直接写二进制数 - 解决的问题:不需要手动转换进制
- 应用场景:位掩码、权限控制、硬件编程
24.5 数字分隔符
- 核心变化:数字字面量中可以用单引号分隔
- 解决的问题:长数字的可读性
- 应用场景:金额、字节数、科学常数
24.6 返回类型推导
- 核心变化:
auto可以推导函数返回类型 - 解决的问题:简化函数声明
- 注意事项:区分
auto和decltype(auto)的类型推导规则
24.7 std::make_unique
- 核心变化:
make_unique加入标准库 - 解决的问题:创建
unique_ptr更安全、更简洁 - 应用场景:独占所有权的资源管理
24.8 带初始化捕获的Lambda
- 核心变化:捕获列表可以使用表达式初始化
- 解决的问题:移动捕获、重命名捕获
- 应用场景:移动语义与Lambda结合
24.9 其他容易被漏掉的特性
[[deprecated]]:大家最常用的属性,其实是 C++14 才进标准的(C++11 只有 [[noreturn]]、[[carries_dependency]])std::exchange:exchange(obj, 新值) 一步完成"取旧值 + 换新值",写移动构造/移动赋值特别顺手std::integer_sequence / std::index_sequence:把参数包变成可用在编译期的下标序列- 带大小的释放函数:
operator delete(void*, std::size_t),让分配器不必再单独记录每块内存的大小 std::quoted:让含空格的字符串"带引号"地写出去、再原样读回来
📝 学习建议:C++14的特性虽然看起来简单,但都是"用时方恨少"的实用工具。建议读者在实践中多使用这些特性,感受它们带来的便利。