目录
- [C++14 的定位:它解决了什么问题](#C++14 的定位:它解决了什么问题)
- 变量模板:让"变量"也拥有模板能力
- [泛型 Lambda:让 Lambda 自动适配不同参数类型](#泛型 Lambda:让 Lambda 自动适配不同参数类型)
- [Lambda 初始化捕获:在捕获列表里直接创建成员](#Lambda 初始化捕获:在捕获列表里直接创建成员)
- [函数返回类型推导:auto 与 decltype(auto)](#函数返回类型推导:auto 与 decltype(auto))
- [二进制字面量:直接用 0b 写位模式](#二进制字面量:直接用 0b 写位模式)
- 数字分隔符:让长数字真正可读
- 带默认成员初始化器的聚合类
- std::exchange:换新值,同时拿走旧值
- [std::make_unique:创建 unique_ptr 的标准方式](#std::make_unique:创建 unique_ptr 的标准方式)
- std::integer_sequence:编译期整数序列
- std::quoted:可靠处理带引号字符串的输入输出
- [std::shared_timed_mutex、std::shared_mutex 与 std::shared_lock](#std::shared_timed_mutex、std::shared_mutex 与 std::shared_lock)
- 字面量后缀与用户定义字面量
- 整套知识应该怎样串起来
- 版本边界速查表
- 参考资料
1. C++14 的定位:它解决了什么问题
C++14 可以看作 C++11 的一次"打磨与补强"。C++11 完成了现代 C++ 的大规模转型:auto、Lambda、右值引用、智能指针、并发库、可变参数模板等能力在那一代集中出现。C++14 没有再进行同等规模的语言重构,而是继续把这些能力变得更顺手、更一致、更容易用于日常代码。
C++14 于 2014 年获批并正式发布。它带来的变化中,和本文直接相关的语言能力主要有:
- 变量模板;
- 泛型 Lambda;
- Lambda 初始化捕获;
- 普通函数返回类型自动推导;
decltype(auto);- 二进制字面量;
- 数字分隔符;
- 聚合类允许默认非静态成员初始化器。
标准库方面,本文重点覆盖:
std::exchange;std::make_unique;std::integer_sequence;std::quoted;std::shared_timed_mutex;std::shared_lock;<string>和<chrono>中的一批标准字面量后缀。
理解这些内容时有一个很重要的思路:不要把每个特性当成孤立的"语法糖"。C++14 很多改进实际上都在做同一件事------让已经存在的泛型、资源管理、值传递、编译期表达能力以及标准库抽象更自然地进入普通代码。
例如,变量模板让"模板"不再只生成类型和函数;泛型 Lambda 让一个匿名函数对象自动拥有类似函数模板的行为;返回类型推导减少了重复类型书写;std::make_unique 让独占式资源管理变得完整;std::exchange 把一个非常常见的"取旧值、写新值"操作标准化;读写锁则补齐了多读少写场景下的并发工具。
因此,学习 C++14 最有效的方法不是记"这一版加了哪些关键词",而是理解每个工具分别在什么问题上减少了重复、降低了出错概率,或者让意图表达得更明确。
2. 变量模板:让"变量"也拥有模板能力
2.1 什么是变量模板
在 C++14 之前,模板最常见的两个形态是类模板和函数模板:
cpp
template <typename T>
struct Box
{
T value;
};
template <typename T>
T add(T a, T b)
{
return a + b;
}
类模板可以根据模板参数生成不同的类型,函数模板可以根据模板参数生成不同的函数。
C++14 又增加了变量模板,这个其实我们在上篇博客中有进行解析:
cpp
template <typename T>
constexpr T pi = T(3.1415926535897932385L);
这句话的含义不是"声明一个类型未知的普通变量",而是:
定义一族变量。模板参数
T不同,就可以得到不同类型的pi实例。
使用:
cpp
pi<float>
pi<double>
pi<long double>
它们是不同的变量模板实例。
2.2 基本语法
变量模板的典型形式是:
cpp
template <模板参数列表>
变量声明;
例如:
cpp
template <typename T>
constexpr T value = T{};
模板参数
这里的:
cpp
typename T
是类型模板参数。
它表示使用变量模板时,需要指定或间接确定一个类型。
变量类型
这里的变量类型是:
cpp
T
也就是说,不同模板实参会生成不同类型的变量。
变量名
cpp
value
是变量模板的名字。
初始化器
cpp
T{}
表示用该类型的值初始化形式产生初始值。
2.3 圆周率变量模板示例
cpp
#include <iostream>
// 定义变量模板 pi。
// T 表示希望使用的数值类型。
//
// pi<float> 的类型是 const float(constexpr 变量具有 const 性质)
// pi<double> 的类型是 const double
// pi<long double> 的类型是 const long double
template <typename T>
constexpr T pi = T(3.1415926535897932385L);
// 函数模板:根据半径计算圆面积。
//
// 参数:
// r ------ 圆的半径,类型为 T。
//
// 返回值:
// 返回 pi<T> * r * r 的计算结果,返回类型为 T。
//
// 使用 pi<T> 的好处是:
// 当 T 是 float 时使用 float 版本;
// 当 T 是 double 时使用 double 版本;
// 不需要分别定义 pi_float、pi_double 等多个常量。
template <typename T>
T circular_area(T r)
{
return pi<T> * r * r;
}
int main()
{
float radius1 = 2.5f;
double radius2 = 2.5;
std::cout << circular_area(radius1) << '\n';
std::cout << circular_area(radius2) << '\n';
}
circular_area 的参数
函数只有一个参数:
cpp
T r
r 表示半径。因为函数是函数模板,所以 T 通常可以通过实参推导。
调用:
cpp
circular_area(2.5f);
编译器可以推导:
text
T = float
调用:
cpp
circular_area(2.5);
则通常推导:
text
T = double
circular_area 的返回值
函数声明:
cpp
T circular_area(T r)
明确表示返回 T。
因此输入 float 时得到 float 结果,输入 double 时得到 double 结果。
2.4 非类型模板参数也可以用于变量模板
变量模板并不要求模板参数一定是类型。
例如阶乘:
cpp
#include <cstddef>
// N 是非类型模板参数。
// 每个不同的 N 都对应一个不同的 factorial<N> 变量。
template <std::size_t N>
constexpr std::size_t factorial =
N * factorial<N - 1>;
// 对 N == 0 的情况进行显式特化。
// 这是递归终止条件:0! = 1。
template <>
constexpr std::size_t factorial<0> = 1;
static_assert(factorial<5> == 120);
这里:
cpp
factorial<5>
不是函数调用,因为后面没有 ()。
它是变量模板的实例。
其逻辑可以理解为:
text
factorial<5>
= 5 * factorial<4>
= 5 * 4 * factorial<3>
= ...
= 5 * 4 * 3 * 2 * 1 * factorial<0>
= 120
factorial 有没有参数和返回值
变量模板不是函数,因此它没有"函数参数"和"函数返回值"。
它有的是:
- 模板参数:
N; - 变量类型:
std::size_t; - 变量值:对应的阶乘结果。
访问方式:
cpp
factorial<5>
而不是:
cpp
factorial(5) // 这不是当前这个变量模板的用法
2.5 变量模板的真正价值
变量模板很适合表达"值本身依赖模板参数"的场景。
可以把三类模板简单区分为:
text
类模板
→ 根据模板参数生成类型
函数模板
→ 根据模板参数生成函数
变量模板
→ 根据模板参数生成变量
C++17 标准库里大量出现的 _v 形式类型特征,例如 std::is_const_v<T>,正是变量模板接口思路的典型体现:相比 ::value,直接用一个变量模板表示结果会更紧凑。
3. 泛型 Lambda:让 Lambda 自动适配不同参数类型
3.1 C++14 之前的 Lambda 参数类型
普通 Lambda 可以写:
cpp
auto add = [](int a, int b)
{
return a + b;
};
这里两个参数类型都固定为 int。
因此它非常明确,但通用性有限。如果希望同一套逻辑同时用于 int、double 或其他支持对应操作的类型,就会希望参数类型可以自动推导。
C++14 允许 Lambda 的参数使用 auto:
cpp
auto add = [](auto a, auto b)
{
return a + b;
};
这就是泛型 Lambda,也就是不再是强制类型的,我们可以同时传入int类型的,也可以同时传入double类型的,都是可以的,不用再去int一套,double一套!!而且因为没有lambda模版,所以这个泛型lambda算是极优解了。
可以把它理解为:
编译器为 Lambda 的调用运算符
operator()生成类似函数模板的能力。
3.2 auto 参数的推导方式
示例:
cpp
#include <iostream>
#include <string>
int main()
{
// a 和 b 的具体类型由调用时的实参决定。
auto get_max = [](const auto& a, const auto& b)
{
return a > b ? a : b;
};
std::cout << get_max(10, 20) << '\n';
std::cout << get_max(
std::string("apple"),
std::string("banana")
) << '\n';
}
第一次调用:
cpp
get_max(10, 20)
两个参数被推导为和 int 对应的引用类型。
第二次调用:
cpp
get_max(std::string("apple"), std::string("banana"))
参数则对应 std::string。
参数为什么写成 const auto&
cpp
const auto& a
可以拆开理解:
auto:由实参推导基础类型;&:按引用接收,避免不必要复制;const:函数只读取参数,不允许通过该引用修改原对象。
对于可能较大的对象,例如 std::string,这种写法通常比直接按值复制更合适。
返回值
这里没有显式写返回类型:
cpp
return a > b ? a : b;
Lambda 的返回类型会根据返回表达式推导。
需要注意,这里的条件运算符最终类型遵循 C++ 自身的类型规则,并不是简单地"永远返回第一个参数的类型"。
3.3 auto&:明确接收左值引用
cpp
auto modify = [](auto& value)
{
value += 1;
};
参数:
cpp
auto& value
表示参数需要绑定到一个可修改的左值对象。
使用:
cpp
int x = 10;
modify(x);
执行后:
text
x == 11
这种形式的意图很直接:函数体要修改外部传入对象。
3.4 auto&&:泛型 Lambda 中的转发引用
在泛型 Lambda 参数位置:
cpp
auto&& x
具有非常强的适配能力。
例如:
cpp
#include <iostream>
int main()
{
auto inspect = [](auto&& x)
{
// 此处 x 可以对应左值实参,也可以对应右值实参。
std::cout << x << '\n';
};
int n = 10;
inspect(n); // 左值
inspect(20); // 右值
}
这种参数形式常用于希望同时接受左值和右值、并尽量保留实参属性的泛型代码,不要忘记了引用折叠哦。
如果后续还要把参数继续转交给其他函数,通常会配合:
cpp
std::forward<decltype(x)>(x)
这也是为什么在泛型 Lambda 里经常能看到:
cpp
decltype(x)
因为参数本身用 auto&& 推导,decltype(x) 可以得到它在当前实例中的精确声明类型。
3.5 参数包:一次接收任意数量参数
C++14 泛型 Lambda 还可以写:
cpp
auto f = [](auto&&... args)
{
// ...
};
其中:
cpp
auto&&... args
表示一个参数包。
它可以接收不同数量、不同类型的实参。
可以看到,其实这个也是和模版很类似的,可以说泛型lambda就是特殊的函数模版,怎么不能说是lambda模版呢。
一个很典型的例子是把参数继续完美转发给容器的 emplace_back:
cpp
#include <string>
#include <utility>
#include <vector>
int main()
{
std::vector<std::string> values;
// 捕获 values 的引用。
//
// ts 是一个参数包:
// 调用时传几个参数,这里就接收几个参数。
//
// std::forward<decltype(ts)>(ts)...
// 会逐个转发参数,尽量保持每个实参原本的左值/右值属性。
auto append = [&values](auto&&... ts)
{
values.emplace_back(
std::forward<decltype(ts)>(ts)...
);
};
append("hello");
// std::string 有"数量 + 字符"构造形式,
// 所以下面构造一个由 10 个 'y' 组成的字符串。
append(10, 'y');
}
append 的参数
参数数量不固定:
cpp
auto&&... ts
调用:
cpp
append("hello");
参数包里有一个元素。
调用:
cpp
append(10, 'y');
参数包里有两个元素。
append 的返回值
函数体没有 return value;,因此它返回 void。
3.6 C++20 的显式模板参数 Lambda:这里只作为版本延伸
C++14 的泛型 Lambda 使用 auto 隐式形成泛型参数。
C++20 又允许直接在 Lambda 中写模板参数列表,这算是直接指明了就是lambda模版~~:
cpp
auto compare = []<typename T>(T a, T b)
{
return a < b;
};
这一语法不是 C++14,而是 C++20,但是说白了,还是这种方式更加直观,更加好理解,大家尽量还是因为以这个为准,\[\](){}
它和 C++14 泛型 Lambda 属于同一条功能演进路线:当简单的 auto 参数不够表达模板参数之间的关系时,显式模板参数列表会更有控制力。
4. Lambda 初始化捕获:在捕获列表里直接创建成员
4.1 普通捕获的限制
Lambda 捕获经常这样写:
cpp
int x = 10;
auto f = [x]()
{
return x;
};
这里捕获的是外部已经存在的 x。
C++14 增加初始化捕获,也叫 generalized lambda capture / init-capture:
cpp
auto f = [y = x * 2]()
{
return y;
};
它允许在捕获列表中直接声明并初始化 Lambda 闭包对象中的成员。
4.2 捕获表达式计算结果
cpp
#include <iostream>
int main()
{
int x = 10;
// y 不是外层原本就存在的变量。
// 它是在捕获列表中创建的闭包成员,
// 初始值来自 x * 2。
auto f = [y = x * 2]()
{
std::cout << y << '\n';
};
f(); // 输出 20
}
关键点:
cpp
[y = x * 2]
左边 y 是 Lambda 内部新建立的名字。
右边:
cpp
x * 2
是初始化表达式。
因此初始化捕获不仅能"改名捕获",还能在捕获时完成计算、类型转换、移动等操作。
4.3 移动捕获:C++14 非常重要的实际用途
初始化捕获的经典应用是把只能移动、不能复制的对象带入 Lambda。
例如 std::unique_ptr:
cpp
#include <iostream>
#include <memory>
#include <utility>
int main()
{
auto ptr = std::make_unique<int>(42);
// std::unique_ptr 不允许复制。
//
// ptr = std::move(ptr)
// 右侧把外部 ptr 的资源所有权移动进 Lambda 闭包对象。
//
// 捕获完成后,外部 ptr 通常变为空;
// Lambda 内部的 ptr 成为资源的新所有者。
auto task = [ptr = std::move(ptr)]()
{
std::cout << *ptr << '\n';
};
task();
}
这里要特别分清两个同名的 ptr:
text
捕获列表右侧 std::move(ptr)
→ 指外部变量
捕获列表左侧 ptr
→ Lambda 闭包对象中新建立的成员
这种能力是初始化捕获非常核心的价值,因为 C++11 的普通按值捕获更接近"复制",而 std::unique_ptr 恰恰不能复制。
4.4 多种捕获可以组合
cpp
#include <iostream>
#include <memory>
#include <vector>
int main()
{
std::vector<int> numbers{1, 2, 3, 4, 5};
auto ptr = std::make_unique<int>(10);
auto f =
[
value = 5, // 初始化捕获一个整数
ptr = std::move(ptr), // 移动捕获 unique_ptr
&numbers // 按引用捕获已有容器
]()
{
std::cout << value << '\n';
std::cout << *ptr << '\n';
std::cout << numbers.size() << '\n';
};
f();
}
这段捕获列表把三种需求放在一起:
value = 5:直接创建并初始化闭包成员;ptr = std::move(ptr):转移资源所有权;&numbers:不复制容器,只保存对外部对象的引用关系。
使用引用捕获时要注意生命周期:Lambda 使用引用时,被引用对象必须仍然存在。
5. 函数返回类型推导:auto 与 decltype(auto)
5.1 C++11 的尾置返回类型
在需要让返回类型依赖参数表达式时,C++11 常见写法是:
cpp
template <typename T>
auto scale(T x) -> decltype(x * 1.5)
{
return x * 1.5;
}
这里:
cpp
-> decltype(x * 1.5)
是真正的返回类型。
尾置返回类型放在参数列表后面,因此返回类型表达式可以直接使用已经声明的参数名字 x
5.2 C++14:普通函数可以直接使用 auto 推导返回类型
C++14 允许:
cpp
auto f()
{
return 10;
}
编译器根据 return 表达式推导返回类型。
例如:
cpp
auto f1()
{
return 10;
}
// 返回 int
auto f2()
{
return 3.14;
}
// 返回 double
参数
这里函数是否有参数与返回类型推导机制没有直接冲突。
例如:
cpp
auto multiply(int x)
{
return x * 1.5;
}
参数:
cpp
int x
返回表达式:
cpp
x * 1.5
因为 1.5 是 double,常规算术转换后结果为 double,所以函数返回类型推导为 double。
5.3 多个 return 必须推导出一致类型
cpp
auto select_value(bool flag)
{
if (flag)
return 1.0; // double
else
return 2; // int
}
这段代码不能因为 int 可以转成 double 就自动选择 double。
auto 返回类型推导要求相关返回语句推导出相同类型。
更清楚的写法是统一返回表达式类型:
cpp
auto select_value(bool flag)
{
if (flag)
return 1.0;
else
return 2.0;
}
此时两个分支都推导为 double。
5.4 auto 与引用
如果写:
cpp
int global_value = 10;
auto get_value()
{
return global_value;
}
返回类型为:
cpp
int
也就是按值返回。
如果明确希望返回引用,可以写:
cpp
auto& get_value_ref()
{
return global_value;
}
返回类型为:
cpp
int&
调用:
cpp
get_value_ref() = 20;
会直接修改 global_value。
因此,函数返回类型前的 & 不是装饰,它决定了调用者拿到的是对象副本还是原对象的引用。
5.5 decltype(auto):按 decltype 规则精确保留类型
C++14 还引入了:
cpp
decltype(auto)
它与普通 auto 最大的差异是:
auto的推导大体遵循模板实参推导规则;decltype(auto)则按照decltype(返回表达式)的规则确定类型。
经典例子:
cpp
int x = 1;
decltype(auto) f1()
{
return x;
}
decltype(auto) f2()
{
return (x);
}
f1 的返回类型
cpp
return x;
对应:
cpp
decltype(x)
对于未加括号的变量名表达式,结果是该实体的声明类型:
text
int
所以 f1() 返回 int。
f2 的返回类型
cpp
return (x);
对应:
cpp
decltype((x))
(x) 是左值表达式,因此得到:
text
int&
所以 f2() 返回 int&。
这也是 C++ 中一个非常容易被忽略的细节:
text
decltype(x)
与
decltype((x))
可能完全不同
5.6 decltype(auto) 常见于"保持被调用函数的返回类型"
cpp
#include <utility>
// F:可调用对象类型
// Args...:参数类型包
//
// 参数:
// f ------ 要调用的函数、函数对象或其他可调用对象。
// args... ------ 要转发给 f 的全部实参。
//
// 返回值:
// 返回 f(args...) 的结果。
// decltype(auto) 会尽量精确保留原调用表达式的返回类型,
// 包括"按值返回"还是"按引用返回"。
template <typename F, typename... Args>
decltype(auto) call(F&& f, Args&&... args)
{
return std::forward<F>(f)(
std::forward<Args>(args)...
);
}
这个函数的重点不是做复杂计算,而是"透明转发"。
如果被调用函数返回 int,call 应该返回 int。
如果被调用函数返回 int&,理想情况下 call 也应该保持 int&。
因此使用 decltype(auto) 比单纯写 auto 更符合这类封装函数的意图。
需要注意:精确保留引用并不自动保证引用安全。 如果底层函数本身返回了悬空引用,decltype(auto) 只会忠实保留这个类型,不会替你修复生命周期问题。
6. 二进制字面量:直接用 0b 写位模式
6.1 四种常见整数写法
同一个十进制值 42 可以写成:
cpp
int d = 42; // 十进制
int o = 052; // 八进制
int h = 0x2A; // 十六进制
int b = 0b101010; // 二进制,C++14
它们表示相同数值,只是源代码中的进制表示方式不同。
C++14 新增的二进制字面量前缀是:
text
0b
或
0B
后面只能出现:
text
0
1
例如:
cpp
0b1111
就是十进制 15。
6.2 二进制字面量最适合位标志
二进制表示最大的优势不是"能写数字",而是位模式一眼就能看出来。
cpp
#include <bitset>
#include <iostream>
int main()
{
// 每一个常量只占一个独立 bit。
constexpr int FLAG_READ = 0b0001;
constexpr int FLAG_WRITE = 0b0010;
constexpr int FLAG_EXECUTE = 0b0100;
constexpr int FLAG_ADMIN = 0b1000;
// 按位或 |:
// 把 READ 与 EXECUTE 两个 bit 同时打开。
int flags = FLAG_READ | FLAG_EXECUTE;
std::cout << std::bitset<4>(flags) << '\n';
// 按位与 &:
// 检查对应 bit 是否存在。
if (flags & FLAG_READ)
{
std::cout << "read enabled\n";
}
if (flags & FLAG_WRITE)
{
std::cout << "write enabled\n";
}
}
std::bitset<4> 的作用
std::bitset<4> 表示固定 4 位的位集合。
构造:
cpp
std::bitset<4>(flags)
把整数 flags 的低 4 位转换成适合显示和位操作的 bitset 对象。
这里主要用于让结果以类似:
text
0101
的形式输出,更容易观察位状态。
6.3 字面量的"进制"不等于变量的"存储进制"
需要建立一个正确观念:
cpp
int x = 42;
int y = 0b101010;
运行时并不存在"x 是十进制整数,y 是二进制整数"这种类型差异。
两者类型都可能是 int,值都为 42。
十进制、二进制、十六进制只是源代码里写这个数的方式。
7. 数字分隔符:让长数字真正可读
7.1 基本语法
C++14 允许在数字字面量的数字之间使用单引号,也就是':
cpp
1'000'000
编译器在确定数值时会忽略这些分隔符。
因此:
cpp
int a = 1000000;
int b = 1'000'000;
两者值完全相同。
分隔符的作用只有一个核心目标:
提高源代码可读性。
7.2 不同进制都可以使用
cpp
int decimal = 1'000'000;
unsigned int hex_value = 0xDEAD'BEEF;
unsigned int binary =
0b1010'1010'0101'0101;
double pi =
3.141'592'653'589'793;
十进制
cpp
1'000'000
按照常见千位分组阅读。
十六进制
cpp
0xDEAD'BEEF
可按 4 个十六进制数字一组阅读。
二进制
cpp
0b1010'1010'0101'0101
可按 4 bit 一组阅读,这种方式与十六进制位组之间的对应关系尤其清晰。
7.3 分隔符不改变类型和值
cpp
auto x = 1000;
auto y = 1'000;
x 和 y 的数值、类型推导没有因为单引号发生变化。
因此不要把单引号理解成字符串符号,也不要理解成特殊的"格式化存储"。
它只是数字记号的一部分。
7.4 推荐按语义分组,而不是随意插入
虽然:
cpp
100'0000
在语法上可以表示 1000000,但阅读习惯非常差。
更自然的是:
cpp
1'000'000
对于二进制:
cpp
0b1111'0000'1010'0101
通常比:
cpp
0b1'111000'010100'101
更容易检查。
因此数字分隔符真正的最佳实践不是"能插就插",而是:
按人类阅读习惯或数据格式的语义边界进行分组。
8. 带默认成员初始化器的聚合类
8.1 什么是聚合初始化
结构体常见写法:
cpp
struct Employee
{
std::string name;
int id;
double salary;
};
可以用花括号按成员顺序初始化:
cpp
Employee e{"Alice", 1001, 8500.0};
这种模式属于聚合初始化的一种典型形式。
C++ 中"聚合类型"的精确定义会随语言版本变化,所以实际判断某个复杂类是不是聚合时,应以对应标准版本的规则为准。本文重点讨论 C++14 的关键变化:
C++14 允许聚合类拥有默认非静态成员初始化器。
8.2 默认成员初始化器
cpp
#include <string>
struct Employee
{
std::string name = "Unknown";
int id = -1;
double salary = 0.0;
};
这里:
cpp
name = "Unknown";
id = -1;
salary = 0.0;
都是默认成员初始化器。
它们表示:
当某个成员没有被其他更直接的初始化方式指定时,可以使用这里给出的默认值。
8.3 部分聚合初始化
cpp
Employee e1{"John"};
成员按照声明顺序匹配:
text
name ← "John"
id ← 没有显式提供,使用默认成员初始化器 -1
salary ← 没有显式提供,使用默认成员初始化器 0.0
再看:
cpp
Employee e2{"Alice", 123};
结果:
text
name = "Alice"
id = 123
salary = 0.0
再看空初始化列表:
cpp
Employee e3{};
结果:
text
name = "Unknown"
id = -1
salary = 0.0
这里特别值得注意:e3{} 并不等价于任意一个已经手动传入不同值的对象。它会按照聚合初始化规则和默认成员初始化器来初始化各成员。
8.4 C++14 普通聚合初始化的限制:按顺序
假设:
cpp
struct Employee
{
std::string name = "Unknown";
int id = -1;
double salary = 0.0;
};
如果想只指定 salary,C++14 的普通聚合初始化没有"直接点名后面的成员、跳过中间成员"的通用语法。
因为普通初始化列表按成员声明顺序对应。
8.5 C++20 指定初始化器:同一主题的后续演进
C++20 增加指定初始化器,使用 .变量名=变量值的形式去指定一个变量的值 :
cpp
Employee e{
.id = 1,
.salary = 1.1
};
这里没有指定 name,它仍然可以使用默认成员初始化器。
但 C++ 的指定初始化器与 C 语言并不完全相同。一个重要规则是:
指定成员的顺序必须和成员声明顺序保持一致。
对于:
cpp
struct Employee
{
std::string name;
int id;
double salary;
};
下面顺序:
cpp
Employee e{
.id = 1,
.salary = 1.1
};
与声明顺序一致。
不要反过来写:
cpp
// 不应这样写:指定顺序与成员声明顺序相反。
// Employee e{
// .salary = 1.1,
// .id = 1
// };
8.6 嵌套聚合
cpp
struct Inner
{
int a;
int b;
};
struct Outer
{
Inner inner;
int c;
};
普通聚合初始化:
cpp
Outer value{
{1, 2},
3
};
其关系是:
text
inner.a = 1
inner.b = 2
c = 3
C++20 指定初始化器可以把成员名字写出来,使层次更清楚:
cpp
Outer value{
.inner = {1, 2},
.c = 3
};
9. std::exchange:换新值,同时拿走旧值
9.1 std::exchange 做什么
std::exchange 定义在:
cpp
#include <utility>
它的核心语义:
用新值替换对象当前的值,同时返回对象替换之前的旧值。
基本声明可理解为:
cpp
template <class T, class U = T>
T exchange(T& obj, U&& new_value);
C++14 引入;C++20 起成为 constexpr;C++23 起拥有条件 noexcept 说明。
9.2 参数逐个解释
调用形式:
cpp
std::exchange(obj, new_value)
第一个参数 obj
对应:
cpp
T& obj
这是要被修改的对象。
它按左值引用接收,因为 std::exchange 必须真正修改原对象。
第二个参数 new_value
对应:
cpp
U&& new_value
表示准备写入 obj 的新值。
U 默认是 T,但它并不被限制为必须与 T 完全相同,只要这个值可以正确赋给 obj 即可。
9.3 返回值
返回类型:
cpp
T
返回的是:
obj被替换之前的旧值。
因此:
cpp
int x = 10;
int old = std::exchange(x, 20);
执行结束后:
text
old == 10
x == 20
9.4 最直观的示例
cpp
#include <iostream>
#include <utility>
int main()
{
int value = 10;
// 处理过程可以理解为:
//
// 1. 先保存 value 的旧值 10;
// 2. 把 20 赋给 value;
// 3. 返回旧值 10。
int old_value = std::exchange(value, 20);
std::cout << "old = " << old_value << '\n';
std::cout << "new = " << value << '\n';
}
9.5 类成员"设置并返回旧值"
cpp
#include <utility>
class StreamState
{
public:
using flags_type = int;
// 读取当前 flags_。
//
// 参数:无。
// 返回值:当前 flags_ 的副本。
flags_type flags() const
{
return flags_;
}
// 设置新的 flags_,并返回修改前的旧值。
//
// 参数:
// new_flags ------ 准备写入的新标志值。
//
// 返回值:
// 修改之前的 flags_。
flags_type flags(flags_type new_flags)
{
return std::exchange(flags_, new_flags);
}
private:
flags_type flags_ = 0;
};
如果:
cpp
StreamState s;
int old = s.flags(12);
那么:
text
old == 0
s.flags() == 12
这比手写:
cpp
auto old = flags_;
flags_ = new_flags;
return old;
更紧凑,而且意图非常明确。
9.6 容器也能交换成新值
cpp
#include <iostream>
#include <utility>
#include <vector>
int main()
{
std::vector<int> values{10, 20, 30};
// values 会变成 {1, 2, 3, 4}。
// old_values 接收原来的 {10, 20, 30}。
std::vector<int> old_values =
std::exchange(values, std::vector<int>{1, 2, 3, 4});
for (int x : values)
std::cout << x << ' ';
}
这里 std::exchange 不是 std::swap。
区别可以这样理解:
text
std::swap(a, b)
→ a 和 b 交换彼此的值
std::exchange(a, new_value)
→ a 被改成 new_value
→ 同时把 a 的旧值作为函数结果返回
9.7 在状态更新表达式中使用
资料中常见的一个例子是斐波那契数列:
cpp
#include <iostream>
#include <utility>
int main()
{
for (int a = 0, b = 1;
a < 100;
a = std::exchange(b, a + b))
{
std::cout << a << ' ';
}
}
更新部分:
cpp
a = std::exchange(b, a + b)
可以拆成:
- 计算新值
a + b; b被替换为这个新值;std::exchange返回b原来的旧值;- 把这个旧值赋给
a。
这种写法很紧凑,但阅读成本比普通三行状态更新高。因此在业务代码中,应优先保证可读性,而不是为了使用 std::exchange 强行压成一行。
10. std::make_unique:创建 unique_ptr 的标准方式
10.1 为什么需要 std::make_unique
std::unique_ptr 在 C++11 已经存在,但 C++11 标准库没有与 std::make_shared 对应的 std::make_unique。
C++14 补上:
cpp
std::make_unique
它定义在:
cpp
#include <memory>
主要作用:
创建动态对象,并立即把对象所有权交给一个
std::unique_ptr。
它让代码避免直接写裸 new,语义也更统一。
10.2 创建单个对象
典型形式:
cpp
template <class T, class... Args>
std::unique_ptr<T> make_unique(Args&&... args);
模板参数 T
表示要创建的对象类型。
例如:
cpp
std::make_unique<int>(42)
这里:
text
T = int
函数参数 args...
这是构造参数包。
它会被转发给 T 的构造过程。
例如:
cpp
std::make_unique<Person>("Alice", 18)
大体表示创建:
cpp
Person("Alice", 18)
对应的动态对象。
返回值
返回:
cpp
std::unique_ptr<T>
这个智能指针独占新对象。
10.3 单对象示例
cpp
#include <iostream>
#include <memory>
struct Vec3
{
int x;
int y;
int z;
// 参数:
// x_value、y_value、z_value 分别用于初始化三个坐标。
//
// 返回值:
// 构造函数没有返回值。
Vec3(int x_value = 0,
int y_value = 0,
int z_value = 0) noexcept
: x(x_value),
y(y_value),
z(z_value)
{
}
};
int main()
{
// 创建一个 int,值初始化得到 0。
auto p1 = std::make_unique<int>();
// 创建一个 int,并用 42 初始化。
auto p2 = std::make_unique<int>(42);
// 创建 Vec3,并把 1、2、3 转发给构造函数。
auto p3 = std::make_unique<Vec3>(1, 2, 3);
std::cout << *p2 << '\n';
std::cout << p3->x << '\n';
}
10.4 auto p = std::make_unique<T>() 为什么常见
可以写完整类型:
cpp
std::unique_ptr<Vec3> p =
std::make_unique<Vec3>(1, 2, 3);
也可以:
cpp
auto p =
std::make_unique<Vec3>(1, 2, 3);
后者通常更简洁,因为右侧已经非常明确地说明:
text
这是一个 std::unique_ptr<Vec3>
使用 auto 并不会让资源所有权含义变得模糊。
10.5 创建动态数组
std::make_unique 对未知边界数组也提供支持:
cpp
auto values = std::make_unique<int[]>(5);
这里:
cpp
int[]
表示数组元素类型为 int。
参数:
cpp
5
表示数组元素个数。
返回类型是:
cpp
std::unique_ptr<int[]>
元素会按 make_unique 对数组的规则进行值初始化,因此对于 int,这里得到的元素初值为 0。
访问:
cpp
values[0]
values[1]
10.6 为什么推荐 make_unique
通常优先写:
cpp
auto p = std::make_unique<T>(args...);
而不是:
cpp
std::unique_ptr<T> p(new T(args...));
主要原因包括:
- 不需要在业务代码中直接出现裸
new; - 创建对象和移交所有权在一个标准接口中完成;
- 模板参数和返回类型关系更清楚;
- 在包含多个函数实参、多个资源创建操作的复杂表达式中,工厂函数形式更容易维持可靠的资源管理;
- 与
std::make_shared形成一致的现代 C++ 创建风格。
需要注意,"直接构造 unique_ptr 一定会泄漏"并不是准确说法。单独写:
cpp
std::unique_ptr<T> p(new T(...));
unique_ptr 本身仍然会管理成功接管的对象。make_unique 的优势是进一步减少裸指针暴露和复杂表达式中的资源管理风险,同时让写法更统一。
10.7 std::make_unique_for_overwrite:C++20 延伸
C++20 增加:
cpp
std::make_unique_for_overwrite
它面向"马上就会完全覆盖写入"的场景,避免不必要的值初始化。
例如:
cpp
auto buffer =
std::make_unique_for_overwrite<int[]>(1000);
这不是 C++14 接口,而是 C++20 的后续补充。
当你确实会立即写满所有元素时,它可以表达"无需先把元素初始化成 0,再覆盖一遍"的意图。
10.8 到 C++23 的状态
std::make_unique 的主要创建重载在 C++23 进一步获得 constexpr 能力。
这不改变 C++14 中最重要的使用习惯:
动态创建由
unique_ptr独占的对象时,优先考虑std::make_unique。
11. std::integer_sequence:编译期整数序列
11.1 它是什么
std::integer_sequence 定义在:
cpp
#include <utility>
C++14 引入。
基本形式:
cpp
template <class T, T... Ints>
struct integer_sequence;
它表示:
一组在编译期就存在的整数,并把这些整数放在一个非类型模板参数包中。
例如:
cpp
std::integer_sequence<int, 2, 4, 6, 8>
可以理解为:
text
元素类型:int
整数序列:2, 4, 6, 8
11.2 模板参数详细解释
cpp
template <class T, T... Ints>
T
表示序列中整数元素的类型。
例如:
cpp
std::integer_sequence<int, ...>
则:
text
T = int
也可以是:
cpp
std::size_t
unsigned
等整数类型。
Ints...
这是非类型模板参数包。
例如:
cpp
std::integer_sequence<unsigned, 9, 2, 5>
里面的:
text
9, 2, 5
就是 Ints...。
11.3 成员类型 value_type
std::integer_sequence 提供:
cpp
using value_type = T;
因此可以获取其元素类型。
例如概念上:
cpp
using Seq =
std::integer_sequence<int, 1, 2, 3>;
using ValueType = Seq::value_type;
// ValueType 为 int
11.4 成员函数 size()
integer_sequence 提供静态成员函数:
cpp
static constexpr std::size_t size() noexcept;
参数
无参数。
返回值
返回序列中的整数数量。
等价于:
cpp
sizeof...(Ints)
例如:
cpp
using Seq =
std::integer_sequence<int, 10, 20, 30>;
static_assert(Seq::size() == 3);
11.5 打印序列:理解参数包如何被"解开"
下面例子使用 C++17 折叠表达式来展示整数序列内容。std::integer_sequence 本身是 C++14,而折叠表达式是 C++17,需要区分版本。
cpp
#include <iostream>
#include <utility>
template <typename T, T... Values>
void print_sequence(
std::integer_sequence<T, Values...> seq)
{
// seq.size():
// 返回 Values... 中元素的数量。
std::cout << "size = " << seq.size() << '\n';
// C++17 折叠表达式。
// 对 Values... 中的每个值重复执行输出。
((std::cout << Values << ' '), ...);
std::cout << '\n';
}
int main()
{
print_sequence(
std::integer_sequence<int, 2, 4, 6, 8>{}
);
}
print_sequence 的参数
参数类型:
cpp
std::integer_sequence<T, Values...>
当传入具体 integer_sequence 时,编译器可以从类型中推导:
T;- 整个整数参数包
Values...。
返回值
返回类型为:
cpp
void
函数只输出内容,不返回数据。
11.6 std::make_integer_sequence
为了避免手写:
cpp
std::integer_sequence<int, 0, 1, 2, 3, 4>
标准库提供:
cpp
std::make_integer_sequence<T, N>
它生成:
text
0, 1, 2, ..., N-1
例如:
cpp
using Seq =
std::make_integer_sequence<int, 5>;
逻辑上得到:
cpp
std::integer_sequence<int, 0, 1, 2, 3, 4>
这里:
T:整数元素类型;N:序列长度;- 结果:一个类型,而不是运行时容器对象。
11.7 std::index_sequence
很多场景都需要:
cpp
std::size_t
类型的索引序列,因此标准库提供别名:
cpp
std::index_sequence<I...>
可以理解为:
cpp
std::integer_sequence<std::size_t, I...>
11.8 std::make_index_sequence<N>
它生成:
text
0, 1, 2, ..., N-1
并且索引类型固定为:
cpp
std::size_t
例如:
cpp
using Indices =
std::make_index_sequence<4>;
逻辑上等价于:
cpp
std::index_sequence<0, 1, 2, 3>
11.9 std::index_sequence_for<Ts...>
这个工具根据类型参数包的元素数量生成索引序列。
例如:
cpp
std::index_sequence_for<int, double, char>
逻辑上得到:
cpp
std::index_sequence<0, 1, 2>
它不关心这些类型具体是什么,只关心:
text
一共有 3 个类型
因此生成 3 个索引。
11.10 用索引序列访问 tuple
std::tuple 的元素通过编译期索引访问:
cpp
std::get<0>(t)
std::get<1>(t)
std::get<2>(t)
当元素数量来自参数包时,std::index_sequence 就很适合生成这些索引。
下面的输出部分使用 C++17 折叠表达式;索引序列本身仍是 C++14 功能。
cpp
#include <iostream>
#include <tuple>
#include <utility>
// Args...:tuple 中的元素类型。
// I...:tuple 对应的索引序列。
//
// 参数:
// t ------ 要读取的 tuple。
// std::index_sequence<I...> ------ 只用于把 I... 带入模板。
//
// 返回值:
// void,仅输出元素。
template <typename... Args, std::size_t... I>
void print_tuple_impl(
const std::tuple<Args...>& t,
std::index_sequence<I...>)
{
// C++17:
// 展开成 std::get<0>(t)、std::get<1>(t)......
((std::cout << std::get<I>(t) << ' '), ...);
std::cout << '\n';
}
template <typename... Args>
void print_tuple(const std::tuple<Args...>& t)
{
// Args... 有几个类型,
// index_sequence_for 就生成几个连续索引。
print_tuple_impl(
t,
std::index_sequence_for<Args...>{}
);
}
这里最值得理解的是:
std::integer_sequence不是拿来替代std::vector<int>的。
它的整数存在于模板参数层面,主要服务于编译期展开、索引映射和参数包处理。
12. std::quoted:可靠处理带引号字符串的输入输出
std::quoted 的作用一句话:
用于流输入输出时处理"带引号的字符串",自动添加/去除引号,并处理转义字符。
需要包含:
#include <iomanip>
基本使用:
#include <iomanip>
#include <iostream>
#include <string>
int main()
{
std::string text = "Hello World";
// 输出时自动加双引号
std::cout << std::quoted(text);
}
输出:
"Hello World"
输入时也可以:
std::string text;
std::cin >> std::quoted(text);
如果输入:
"Hello World"
那么:
text == "Hello World"
常用形式
std::quoted(str)
std::quoted(str, delim)
std::quoted(str, delim, escape)
参数作用:
str
→ 要处理的字符串
delim
→ 字符串分隔符,默认是 "
escape
→ 转义字符,默认是 \
例如自定义单引号:
std::string text = "hello world";
std::cout << std::quoted(text, '\'');
输出:
'hello world'
返回值
std::quoted() 返回的是一个用于流输入输出的代理对象,通常不需要关心它的具体类型,直接这样使用:
std::cout << std::quoted(text);
input >> std::quoted(text);
可以直接记成:
std::quoted↓输出时自动加引号 + 处理转义输入时自动解析引号 + 还原字符串
一句话:
std::quoted= 专门帮助iostream安全处理带空格、引号和转义字符的字符串。
12.1 它解决什么问题
普通流输入:
cpp
std::cin >> text;
默认会把空白当成分隔。
如果输入:
text
Hello World
通常只能先读到:
text
Hello
而很多配置、简单文本协议或序列化场景希望:
text
"Hello World"
被当成一个完整字符串,并且还要正确处理内部引号和转义符。
C++14 的:
cpp
std::quoted
正是为这种带分隔符字符串的流输入输出提供标准支持。
它定义在:
cpp
#include <iomanip>
12.2 基本调用形式
输出:
cpp
out << std::quoted(text);
输入:
cpp
in >> std::quoted(text);
std::quoted 并不是简单返回一个新 std::string。
它返回一个实现定义/未指定具体类型的代理对象,使流的 operator<< 或 operator>> 按 quoted 规则工作。
12.3 参数
常见形式可以理解为:
cpp
std::quoted(s, delim, escape)
s
要输出或接收的字符串。
delim
分隔字符,默认是:
cpp
'"'
也就是双引号。
escape
转义字符,默认是:
cpp
'\\'
也就是反斜杠。
12.4 返回值
std::quoted(...) 返回一个"未指定具体类型"的对象。
这意味着通常不要写:
cpp
SomeType x = std::quoted(text);
也不需要知道这个代理对象到底叫什么类型。
标准使用方式就是直接嵌入流表达式:
cpp
std::cout << std::quoted(text);
或:
cpp
input >> std::quoted(text);
12.5 输出时发生什么
cpp
#include <iomanip>
#include <iostream>
#include <string>
int main()
{
std::string text = "Hello, World!";
std::cout << std::quoted(text) << '\n';
}
输出类似:
text
"Hello, World!"
如果字符串内部包含分隔符或转义字符,std::quoted 会按规则添加转义字符。
例如:
cpp
std::string text =
R"(He said "Hello")";
std::cout << std::quoted(text);
会形成能够区分外层引号和内部引号的输出表示。
12.6 输入时发生什么
cpp
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
int main()
{
std::istringstream input(
R"("Hello, World!")"
);
std::string text;
input >> std::quoted(text);
std::cout << text << '\n';
}
结果:
text
Hello, World!
如果输入第一个字符就是指定分隔符,std::quoted 会读取到对应的未转义结束分隔符,并处理转义。
如果输入并不是以分隔符开头,它会退回到类似普通字符串提取的行为。
因此把它理解成"永远强制要求输入必须带引号"并不准确。
12.7 自定义分隔符和转义字符
cpp
#include <iomanip>
#include <iostream>
#include <string>
int main()
{
std::string text = "It's a test";
// 第二个参数:分隔符
// 这里改成单引号。
std::cout
<< std::quoted(text, '\'')
<< '\n';
// 还可以进一步指定第三个参数作为转义字符。
}
三个参数关系:
text
std::quoted(
字符串,
分隔字符,
转义字符
)
12.8 一个简单的"写出再读回"示例
cpp
#include <iomanip>
#include <sstream>
#include <string>
int main()
{
std::string original =
R"(He said "Hello" and left)";
std::stringstream stream;
// 把字符串编码为带分隔符和必要转义的文本。
stream << std::quoted(original);
std::string restored;
// 再按 quoted 规则读回。
stream >> std::quoted(restored);
// restored 应恢复原字符串内容。
}
这种"可逆"处理能力比简单手工:
cpp
'"' + text + '"'
可靠得多,因为手工拼接不会自动处理字符串内部本身存在的引号和转义字符。
12.9 用于简单配置序列化时要知道边界
可以把多个字段写成:
cpp
oss << std::quoted(config.username) << ' '
<< std::quoted(config.server) << ' '
<< config.id;
对应读取:
cpp
iss >> std::quoted(config.username)
>> std::quoted(config.server)
>> config.id;
这种方式非常适合演示流输入输出、保存简单数据或内部小工具。
但不要把它误认为完整的通用序列化协议。
如果涉及:
- 版本兼容;
- 字段缺失;
- 字段重命名;
- 数据校验;
- 二进制兼容;
- 不可信输入;
- 复杂嵌套结构;
就需要更完整的格式和错误处理设计。
13. std::shared_timed_mutex、std::shared_mutex 与 std::shared_lock
13.1 为什么普通 mutex 有时不够
std::mutex 是独占锁。
只要一个线程持有它,其他线程就不能同时持有。
对于"修改操作",独占通常非常合理。
但有些共享数据具有这样的特点:
text
读取非常多
写入比较少
多个读取者同时读是安全的
只要写入时没有其他读者或写者即可
例如一个只偶尔更新、但被大量线程查询的缓存。
这种场景可以考虑共享互斥量。
13.2 两种访问模式
共享互斥量拥有两个主要模式。
共享模式
多个线程可以同时拥有共享锁。
适合只读访问。
独占模式
同一时刻只有一个线程可以独占,并且不能同时存在共享持有者。
适合写操作。
可以形象理解为:
text
读:
读线程 A ─┐
读线程 B ─┼─ 可以同时进行
读线程 C ─┘
写:
写线程 W ── 必须单独占有
13.3 std::shared_timed_mutex
定义在:
cpp
#include <shared_mutex>
C++14 引入。
它支持:
独占锁操作
cpp
lock()
try_lock()
try_lock_for()
try_lock_until()
unlock()
共享锁操作
cpp
lock_shared()
try_lock_shared()
try_lock_shared_for()
try_lock_shared_until()
unlock_shared()
13.4 std::shared_mutex
std::shared_mutex 是 C++17 引入的。
它同样提供共享与独占两种模式,但不提供 shared_timed_mutex 那套 *_for / *_until 超时锁接口。
因此如果项目使用 C++17 或更高标准,并且完全不需要超时锁定,通常会优先考虑语义更直接的:
cpp
std::shared_mutex
但性能不能只凭类型名字下结论。读写锁是否比普通 std::mutex 更快,取决于平台实现、竞争程度、临界区长度、读写比例等因素。读操作很多不代表使用共享锁一定更快,实际工程仍应测量。
13.5 为什么推荐 RAII 锁对象
虽然可以手工:
cpp
mutex.lock_shared();
// ...
mutex.unlock_shared();
但现代 C++ 更推荐用 RAII 对象管理锁生命周期。
共享锁:
cpp
std::shared_lock
独占锁:
cpp
std::unique_lock
或在简单场景使用相应的锁守卫。
好处是:
只要锁对象离开作用域,它的析构函数就会负责释放所拥有的锁。
这样可以显著降低"中途 return、异常或分支导致忘记 unlock"的风险。
13.6 std::shared_lock
std::shared_lock 同样从 C++14 开始提供,定义在:
cpp
#include <shared_mutex>
基本形式:
cpp
template <class Mutex>
class shared_lock;
模板参数:
text
Mutex
表示被管理的共享互斥量类型。
例如:
cpp
std::shared_lock<std::shared_timed_mutex>
13.7 std::shared_lock 常用构造方式
最普通:
cpp
std::shared_lock lock(mutex);
在 C++14 需要写明确模板类型时通常类似:
cpp
std::shared_lock<std::shared_timed_mutex> lock(mutex);
构造成功后,它以共享模式拥有互斥量。
离开作用域自动释放。
13.8 owns_lock()
std::shared_lock 提供:
cpp
bool owns_lock() const noexcept;
参数
无。
返回值
如果当前锁对象拥有互斥量,返回:
text
true
否则:
text
false
这在 try_to_lock 或定时尝试锁定后非常有用。
13.9 一个线程安全计数器
下面用 std::shared_timed_mutex 展示读共享、写独占。
cpp
#include <chrono>
#include <mutex>
#include <shared_mutex>
class ThreadSafeCounter
{
public:
// 读取当前计数。
//
// 参数:无。
//
// 返回值:
// 返回当前 value_ 的副本。
//
// 为什么使用 shared_lock:
// 这个函数只读取 value_;
// 多个线程可以同时执行 get()。
unsigned int get() const
{
std::shared_lock<std::shared_timed_mutex>
lock(mutex_);
return value_;
}
// 将计数加 1。
//
// 参数:无。
// 返回值:void。
//
// 为什么使用 unique_lock:
// ++value_ 会修改共享数据;
// 写入时必须独占互斥量。
void increment()
{
std::unique_lock<std::shared_timed_mutex>
lock(mutex_);
++value_;
}
// 尝试立即获得独占锁。
//
// 参数:无。
//
// 返回值:
// true ------ 成功拿到锁并完成自增;
// false ------ 当前无法获得独占锁,没有修改 value_。
bool try_increment()
{
std::unique_lock<std::shared_timed_mutex>
lock(mutex_, std::try_to_lock);
if (!lock.owns_lock())
{
return false;
}
++value_;
return true;
}
// 在指定时间范围内尝试获得独占锁。
//
// 参数:
// timeout ------ 最长等待时间。
//
// 返回值:
// true ------ 在等待范围内获得锁并完成自增;
// false ------ 超时或未获得锁,没有执行自增。
bool try_increment_for(
std::chrono::milliseconds timeout)
{
std::unique_lock<std::shared_timed_mutex>
lock(mutex_, timeout);
if (!lock.owns_lock())
{
return false;
}
++value_;
return true;
}
private:
// get() 是 const 成员函数,
// 但加锁会改变 mutex_ 自身内部状态。
//
// mutable 允许在 const 成员函数中修改这个同步成员,
// 同时不意味着 value_ 本身可以被 const 函数修改。
mutable std::shared_timed_mutex mutex_;
unsigned int value_ = 0;
};
13.10 get() 为什么是 const,但 mutex_ 又是 mutable
这一点非常重要。
cpp
unsigned int get() const
从"业务数据语义"看,它只是读取 value_,所以应该是 const 成员函数。
但为了线程安全,它必须:
cpp
lock(mutex_);
加锁会改变互斥量内部状态。
如果 mutex_ 不是 mutable,const 成员函数里就无法进行这种修改。
因此:
cpp
mutable std::shared_timed_mutex mutex_;
表达的是:
互斥量是同步机制的一部分,它的内部锁状态变化不等于业务对象的逻辑值发生改变。
13.11 try_increment_for 的参数设计
比起传:
cpp
int milliseconds
更现代、更不容易搞错单位的方式是直接传:
cpp
std::chrono::milliseconds
调用:
cpp
counter.try_increment_for(
std::chrono::milliseconds(30)
);
如果启用了 C++14 chrono 字面量,也可以写:
cpp
using namespace std::chrono_literals;
counter.try_increment_for(30ms);
这样函数接口直接携带时间单位,比裸整数更清楚。
13.12 <syncstream> 不是 C++14
多线程输出示例中有时会用:
cpp
#include <syncstream>
std::osyncstream
需要明确:
<syncstream>/std::osyncstream是 C++20 内容,不属于 C++14。
它适合防止多个线程的输出片段互相穿插。
如果代码目标严格是 C++14,就不能直接依赖这个组件。
14. 字面量后缀与用户定义字面量
字面量后缀与用户定义字面量可以分成两部分理解:
字面量后缀用来说明"这个字面量是什么类型或具有什么语义";用户定义字面量则允许自己定义新的后缀。
1. 普通字面量后缀
C++ 本身就有很多后缀:
auto a = 10; // int
auto b = 10u; // unsigned int
auto c = 10L; // long
auto d = 10UL; // unsigned long
auto e = 10LL; // long long
auto f = 10ULL; // unsigned long long
auto g = 3.14; // double
auto h = 3.14f; // float
auto i = 3.14L; // long double
作用可以理解成:
10
↓
默认整数类型
10u
↓
unsigned int
10LL
↓
long long
3.14
↓
double
3.14f
↓
float
也就是说:
普通后缀主要用于明确字面量本身的类型。
2. 用户定义字面量
C++ 允许自己定义后缀,通过:
operator"" _后缀
实现。通用形式是:
ReturnType operator"" _YourSuffix(Parameters);
其中:
ReturnType
→ 使用这个字面量后最终得到的类型
_YourSuffix
→ 自定义后缀名
Parameters
→ 接收字面量原始值或字符数据
自定义后缀通常以下划线开头,例如:
_km
_pi_s
资料也特别强调,自定义后缀应使用 _ 开头,避免和标准库保留的后缀冲突。
3. 整数字面量示例
例如定义:
// 把"公里"转换成"米"
constexpr long double operator"" _km(
unsigned long long value)
{
// value:
// 例如写 5_km 时,这里 value == 5
//
// 返回值:
// 返回 long double 类型的米数
return value * 1000.0L;
}
使用:
auto distance = 5_km;
相当于:
auto distance = 5000.0L;
这里:
5
↓
传给 value
_km
↓
调用 operator"" _km
返回
↓
5000.0L
14.1 普通整数、浮点数字面量本来就有后缀
例如:
cpp
auto a = 10; // int
auto b = 10u; // unsigned int
auto c = 10L; // long
auto d = 10UL; // unsigned long
auto e = 10LL; // long long
auto f = 10ULL; // unsigned long long
auto g = 3.14; // double
auto h = 3.14f; // float
auto i = 3.14L; // long double
这些后缀的作用是:
参与确定字面量本身的类型。
例如:
cpp
10
通常首先尝试 int。
而:
cpp
10u
明确带有 unsigned 后缀。
14.2 用户定义字面量是什么
从 C++11 开始,程序可以定义自己的字面量后缀运算符:
cpp
operator""_suffix
于是:
cpp
5_km
这样的表达式可以拥有程序自定义的语义。
最重要的思想是:
让"数字 + 单位"或"字符串 + 语义"直接在源代码字面量层面形成一个有意义的值。
14.3 自定义后缀命名
自定义用户字面量后缀通常应该使用以下划线开始的名字,例如:
cpp
_km
_deg
_id
不要自定义标准保留的无下划线后缀。
同时也应遵守 C++ 标识符的保留命名规则,避免使用诸如"双下划线"或"下划线后紧跟大写字母"这类保留形式。
14.4 整数字面量操作符:公里转米
cpp
// 这个用户定义字面量接收整数形式,例如:5_km。
//
// 参数:
// value ------ 字面量中的整数数值。
// 例如 5_km 中 value == 5。
//
// 返回值:
// 返回 long double,单位按这里的设计转换为"米"。
//
// constexpr:
// 允许满足条件时在编译期计算这个简单换算。
constexpr long double
operator"" _km(unsigned long long value)
{
return value * 1000.0L;
}
int main()
{
auto distance = 5_km;
// distance == 5000.0L
}
为什么参数是 unsigned long long
用户定义整数字面量操作符有标准规定的可用形式。
这种形式直接接收已经解析后的整数值,因此:
cpp
5_km
传入的数值就是 5。
14.5 浮点字面量操作符
cpp
constexpr long double
operator"" _pi(long double value)
{
return value *
3.14159265358979323846L;
}
int main()
{
auto angle = 2.0_pi;
}
参数:
cpp
long double value
对应浮点字面量的数值。
返回值:
cpp
long double
这里返回自定义计算结果。
14.6 字符串用户定义字面量
cpp
#include <cstddef>
#include <string>
// 参数:
// str ------ 指向字面量字符数据起始位置的指针。
// len ------ 字符串字面量长度。
//
// 返回值:
// 返回一个 std::string,复制指定长度的字符数据。
//
// 使用长度而不是单纯依赖 '\0' 的好处是:
// 字面量内部即使包含 '\0',仍然可以按长度保留完整数据。
std::string
operator"" _text(
const char* str,
std::size_t len)
{
return std::string(str, len);
}
int main()
{
auto s1 = "hello"_text;
auto s2 = "Hello\0World"_text;
}
这里 s2 特别有意义。
普通 C 风格字符串接口经常把第一个 '\0' 视为结束。
而:
cpp
std::string(str, len)
明确使用长度,因此可以保留字面量中的嵌入空字符。
14.7 std::string_view 版本是 C++17
如果写:
cpp
#include <string_view>
并返回:
cpp
std::string_view
需要知道 std::string_view 是 C++17 标准库类型,不属于 C++14。
因此如果目标编译标准严格是 C++14,不应该把它当成 C++14 可直接使用的标准组件。
14.8 C++14 标准字符串字面量 "..."s
C++14 标准库在 <string> 中提供字符串字面量后缀:
cpp
"s"
例如:
cpp
#include <iostream>
#include <string>
using namespace std::string_literals;
int main()
{
auto text = "Hello"s;
// text 的类型是 std::string,
// 而不是 const char*。
std::cout << text.size() << '\n';
}
也可以通过:
cpp
using namespace std::literals;
让相应标准字面量进入查找范围。
14.9 "Hello"s 与 "Hello" 的区别
cpp
auto a = "Hello";
a 推导结果与字符数组退化等规则有关,常见使用层面可视为指向字符数据的指针类型。
而:
cpp
auto b = "Hello"s;
在启用对应标准字面量命名空间后,b 是:
cpp
std::string
因此可以直接调用:
cpp
b.size()
b.append(...)
b += ...
14.10 C++14 chrono 时间字面量
C++14 <chrono> 提供时间单位字面量。
例如:
cpp
#include <chrono>
#include <thread>
using namespace std::chrono_literals;
int main()
{
std::this_thread::sleep_for(500ms);
}
这里:
cpp
500ms
形成:
cpp
std::chrono::milliseconds
类型的时间长度对象,而不是一个"默认单位不清楚的整数"。
14.11 operator""ms 的参数与返回值
对于整数毫秒字面量,标准库的概念形式类似:
cpp
constexpr std::chrono::milliseconds
operator""ms(unsigned long long ms);
参数:
text
ms
→ 字面量数值,例如 500ms 中的 500
返回:
cpp
std::chrono::milliseconds
对于浮点形式:
cpp
1.5ms
会得到能够表达浮点计数的 duration 类型。
14.12 为什么单位字面量比裸整数好
比较:
cpp
sleep_for(std::chrono::milliseconds(500));
与:
cpp
sleep_for(500ms);
后者直接把:
text
数值:500
单位:毫秒
写在一个表达式中。
更重要的是,它不会把"500 到底是秒、毫秒还是微秒"留给函数文档或变量名猜测。
这就是字面量后缀非常实际的价值:
把"值的语义"直接放进类型和表达式中。
15. 整套知识应该怎样串起来
如果把本文所有内容放在一起,会发现 C++14 的这些功能并不是杂乱无章的。
第一条线:让泛型代码更自然
变量模板:
text
模板不仅能生成类和函数
还可以生成变量
泛型 Lambda:
text
Lambda 参数可以自动推导
匿名函数也能自然适配多种类型
返回类型推导:
text
当返回类型明显可以从表达式得到时
减少重复声明
std::integer_sequence:
text
把整数索引带进模板参数层面
方便参数包和 tuple 等编译期结构展开
这几项都在让"泛型编程"从复杂模板技巧逐步进入更普通的代码表达方式。
第二条线:让资源管理更完整
初始化捕获:
text
可以把 unique_ptr 等只能移动的资源直接移动进 Lambda
std::make_unique:
text
让 unique_ptr 的创建不再需要直接裸写 new
std::exchange:
text
让状态转移、取旧换新等常见操作更明确
它们都在强化现代 C++ 非常重要的一条原则:
资源所有权应该由对象表达,而不是散落在手工管理代码中。
第三条线:让源代码更接近人的思维
二进制字面量:
cpp
0b0101
直接展示位模式。
数字分隔符:
cpp
1'000'000
直接展示数字分组。
时间字面量:
cpp
500ms
直接展示数值和单位。
字符串字面量:
cpp
"Hello"s
直接表达希望得到 std::string。
这些能力不会神奇地改变机器底层工作方式,但它们显著提高了源代码表达力。
第四条线:补齐标准库中的高频工具
std::quoted:
text
让带空格、引号、转义的字符串能可靠地进出流
std::shared_timed_mutex 与 std::shared_lock:
text
为多读少写场景提供共享所有权锁模型
它们体现了标准库的一个发展方向:
把开发者反复手写、而且很容易写错的基础模式变成标准组件。
16. 版本边界速查表
| 内容 | 首次标准版本 | 说明 |
|---|---|---|
| 变量模板 | C++14 | template <typename T> constexpr T value = ...; |
泛型 Lambda 的 auto 参数 |
C++14 | [](auto x) { ... } |
| Lambda 初始化捕获 | C++14 | [x = expr] { ... } |
| Lambda 显式模板参数列表 | C++20 | []<typename T>(T x) { ... } |
普通函数 auto 返回类型推导 |
C++14 | auto f() { return ...; } |
decltype(auto) |
C++14 | 按 decltype 规则推导 |
| 二进制字面量 | C++14 | 0b1010 |
| 数字分隔符 | C++14 | 1'000'000 |
| 聚合类允许默认成员初始化器 | C++14 | C++14 放宽聚合定义 |
| 指定初始化器 | C++20 | .member = value |
std::exchange |
C++14 | C++20 起 constexpr,C++23 起条件 noexcept |
std::make_unique |
C++14 | C++23 进一步获得 constexpr 能力 |
std::make_unique_for_overwrite |
C++20 | 面向无需值初始化、随后立即覆盖的场景 |
std::integer_sequence |
C++14 | 编译期整数参数包工具 |
| 折叠表达式 | C++17 | ((expr), ...),不是 C++14 |
std::quoted |
C++14 | <iomanip> |
std::shared_timed_mutex |
C++14 | 支持共享/独占和超时锁 |
std::shared_lock |
C++14 | 共享锁 RAII 包装器 |
std::shared_mutex |
C++17 | 无 timed 接口的共享互斥量 |
std::osyncstream / <syncstream> |
C++20 | 多线程同步输出 |
| 用户定义字面量 | C++11 | 自定义 operator""_suffix |
标准 std::string 的 "..."s |
C++14 | <string> |
chrono 的 ms 等时间字面量 |
C++14 | <chrono> |
std::string_view |
C++17 | 不是 C++14 标准库类型 |
这张表很重要,因为很多示例为了展示"同一功能后来怎么演进",会把 C++17 或 C++20 语法放进 C++14 主题旁边。如果没有明确的版本意识,很容易出现:
text
知道代码怎么写
但不知道为什么自己的 -std=c++14 编译不过
实际项目里最好始终先确定编译标准,例如:
text
C++14
C++17
C++20
C++23
再判断某个语法或标准库设施能否直接使用。
17. 参考资料
本文涉及的版本边界、接口声明和当前标准状态以标准资料与 cppreference 的对应页面进行核对。下面列出与正文主题直接相关的页面,便于继续查阅完整规范信息。
-
C++14 总览
-
变量模板
https://en.cppreference.com/w/cpp/language/variable_template.html
-
Lambda 表达式
-
函数声明与返回类型推导
-
auto/decltype(auto)占位类型 -
整数字面量、二进制字面量、数字分隔符
https://en.cppreference.com/w/cpp/language/integer_literal.html
-
聚合初始化
https://en.cppreference.com/w/cpp/language/aggregate_initialization.html
-
std::exchange -
std::make_unique/std::make_unique_for_overwritehttps://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique.html
-
std::integer_sequencehttps://en.cppreference.com/w/cpp/utility/integer_sequence.html
-
std::quoted -
std::shared_timed_mutexhttps://en.cppreference.com/w/cpp/thread/shared_timed_mutex.html
-
std::shared_mutex -
std::shared_lock -
用户定义字面量
https://en.cppreference.com/w/cpp/language/user_literal.html
-
std::string字面量operator""shttps://en.cppreference.com/w/cpp/string/basic_string/operator""s.html
-
chrono 毫秒字面量
operator""ms
结语
C++14 的价值并不在于某一个特别庞大的新系统,而在于它把 C++11 已经开启的"现代 C++"路线继续铺平。
变量模板让模板可以直接描述值;泛型 Lambda 让匿名函数自然获得泛型能力;初始化捕获让 Lambda 可以安全地接管移动资源;auto 与 decltype(auto) 让函数返回类型的表达更灵活;二进制字面量和数字分隔符提高了源码可读性;聚合类默认成员初始化让简单数据结构既保留花括号初始化,又拥有可靠默认值;std::exchange、std::make_unique、std::integer_sequence 和 std::quoted 把常见基础模式标准化;共享互斥量则让标准并发工具能够直接表达多读少写的访问模型;标准与用户定义字面量进一步让"值 + 语义"在代码中紧密结合。
真正掌握这些内容,不是看到语法能认出来,而是能够回答几个实际问题:
- 这个功能解决的具体重复劳动是什么?
- 它的模板参数、函数参数和返回值分别代表什么?
- 它改变的是类型、值、所有权、初始化方式,还是并发访问方式?
- 它属于 C++14 本身,还是后续标准对同一主题的扩展?
- 在现代代码里,什么时候应该优先使用标准工具,而不是自己重新写一套?
- 一个看起来更短的写法,是否真的比清晰、可靠的普通写法更好?
当这些问题都有明确答案时,C++14 就不再是一串"版本新增特性",而会变成一套可以真正落到项目代码中的语言与标准库工具。