对于 C++:C++14中从变量模板、泛型 Lambda 到并发与字面量的解析

目录

  1. [C++14 的定位:它解决了什么问题](#C++14 的定位:它解决了什么问题)
  2. 变量模板:让"变量"也拥有模板能力
  3. [泛型 Lambda:让 Lambda 自动适配不同参数类型](#泛型 Lambda:让 Lambda 自动适配不同参数类型)
  4. [Lambda 初始化捕获:在捕获列表里直接创建成员](#Lambda 初始化捕获:在捕获列表里直接创建成员)
  5. [函数返回类型推导:auto 与 decltype(auto)](#函数返回类型推导:auto 与 decltype(auto))
  6. [二进制字面量:直接用 0b 写位模式](#二进制字面量:直接用 0b 写位模式)
  7. 数字分隔符:让长数字真正可读
  8. 带默认成员初始化器的聚合类
  9. std::exchange:换新值,同时拿走旧值
  10. [std::make_unique:创建 unique_ptr 的标准方式](#std::make_unique:创建 unique_ptr 的标准方式)
  11. std::integer_sequence:编译期整数序列
  12. std::quoted:可靠处理带引号字符串的输入输出
  13. [std::shared_timed_mutex、std::shared_mutex 与 std::shared_lock](#std::shared_timed_mutex、std::shared_mutex 与 std::shared_lock)
  14. 字面量后缀与用户定义字面量
  15. 整套知识应该怎样串起来
  16. 版本边界速查表
  17. 参考资料

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

因此它非常明确,但通用性有限。如果希望同一套逻辑同时用于 intdouble 或其他支持对应操作的类型,就会希望参数类型可以自动推导。

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.5double,常规算术转换后结果为 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)...
    );
}

这个函数的重点不是做复杂计算,而是"透明转发"。

如果被调用函数返回 intcall 应该返回 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;

xy 的数值、类型推导没有因为单引号发生变化。

因此不要把单引号理解成字符串符号,也不要理解成特殊的"格式化存储"。

它只是数字记号的一部分。


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)

可以拆成:

  1. 计算新值 a + b
  2. b 被替换为这个新值;
  3. std::exchange 返回 b 原来的旧值;
  4. 把这个旧值赋给 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...));

主要原因包括:

  1. 不需要在业务代码中直接出现裸 new
  2. 创建对象和移交所有权在一个标准接口中完成;
  3. 模板参数和返回类型关系更清楚;
  4. 在包含多个函数实参、多个资源创建操作的复杂表达式中,工厂函数形式更容易维持可靠的资源管理;
  5. 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>{}
    );
}

参数类型:

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_ 不是 mutableconst 成员函数里就无法进行这种修改。

因此:

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_mutexstd::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 的价值并不在于某一个特别庞大的新系统,而在于它把 C++11 已经开启的"现代 C++"路线继续铺平。

变量模板让模板可以直接描述值;泛型 Lambda 让匿名函数自然获得泛型能力;初始化捕获让 Lambda 可以安全地接管移动资源;autodecltype(auto) 让函数返回类型的表达更灵活;二进制字面量和数字分隔符提高了源码可读性;聚合类默认成员初始化让简单数据结构既保留花括号初始化,又拥有可靠默认值;std::exchangestd::make_uniquestd::integer_sequencestd::quoted 把常见基础模式标准化;共享互斥量则让标准并发工具能够直接表达多读少写的访问模型;标准与用户定义字面量进一步让"值 + 语义"在代码中紧密结合。

真正掌握这些内容,不是看到语法能认出来,而是能够回答几个实际问题:

  • 这个功能解决的具体重复劳动是什么?
  • 它的模板参数、函数参数和返回值分别代表什么?
  • 它改变的是类型、值、所有权、初始化方式,还是并发访问方式?
  • 它属于 C++14 本身,还是后续标准对同一主题的扩展?
  • 在现代代码里,什么时候应该优先使用标准工具,而不是自己重新写一套?
  • 一个看起来更短的写法,是否真的比清晰、可靠的普通写法更好?

当这些问题都有明确答案时,C++14 就不再是一串"版本新增特性",而会变成一套可以真正落到项目代码中的语言与标准库工具。

相关推荐
土司大王1 小时前
LeetCode hot100——合并两个有序链表
算法·leetcode·链表
码行山野赴时序归途1 小时前
算法效率的度量(上):时间复杂度中的对数思维
c语言·数据结构·算法
算AI2 小时前
基于LLM的无人机仿真测试:新方法竞赛夺佳绩
人工智能·深度学习·算法·机器学习·ai
kaixin_啊啊3 小时前
专用优化算法LKH
算法
一木 之林3 小时前
四、STL容器与数据结构
开发语言·数据结构·c++
程与留3 小时前
07_QMainWindow 与应用程序框架——菜单栏、工具栏、状态栏
c++·qt
_Narcissus_4 小时前
二分算法笔记及例题
数据结构·c++·笔记·算法·蓝桥杯·查找·二分算法
tachibana25 小时前
RAGAS 指标解读
数据库·人工智能·算法·机器学习·架构·大模型·llm
qq_419563095 小时前
ToT 的 BFS/DFS 有个致命缺口:蒙特卡洛树搜索(MCTS)用「随机试错+统计」让大模型想得更深,小模型 + 它竟超过 GPT-4
算法·深度优先·宽度优先