📑 C++ 演进系列总览 | C++11 | C++14 | C++17 | C++20 | C++23 | C++26

C++14 是一个较小的更新,主要是对 C++11 的改进和补充。它没有引入颠覆性概念,而是填补 C++11 留下的缺口、放宽不必要的限制、补齐明显缺失的工具(如 make_unique)。这种"查漏补缺"的版本让 C++11 的新特性更易用、更完整。

泛型 Lambda

C++11 的 Lambda 参数必须指定具体类型,写泛型回调时要靠模板或重载。C++14 允许 Lambda 参数用 auto,编译器为每种调用生成对应的特化——相当于编译器替你写了一个 operator() 模板。这让 Lambda 能像模板函数一样处理多种类型。

1
2
3
4
5
6
7
// C++11 只能指定具体类型
auto lambda1 = [](int x) { return x * 2; };

// C++14 支持 auto 参数
auto lambda2 = [](auto x) { return x * 2; };
lambda2(3); // int
lambda2(3.14); // double

泛型 Lambda 在传给泛型算法、处理异质容器时特别有用。注意每个不同的参数类型组合都会实例化一份代码,可能增大二进制体积。C++20 进一步允许 Lambda 显式写模板参数列表,能表达比 auto 更精细的约束。

Lambda 捕获初始化

C++11 的 Lambda 只能捕获已存在的变量(按值或按引用),无法在捕获时做计算或移动。C++14 的"初始化捕获"([name = expr])允许在捕获时初始化一个新变量——可以是表达式的结果、move 进来的对象、甚至 this 的拷贝。

这解决了几个痛点:捕获移动-only 类型(如 unique_ptr)、捕获表达式的值而非引用(避免悬挂)、按需捕获(如 [ptr = std::move(ptr)] 转移所有权)。捕获的变量成为 Lambda 闭包的成员。

1
2
3
4
5
6
7
int x = 42;
auto lambda = [y = x + 1]() { return y; };
// y 是 43,即使 x 改变也不影响

// 更复杂的捕获
auto ptr = std::make_unique<int>(100);
auto capturePtr = [p = std::move(ptr)]() { return *p; };

第一个例子:y 是捕获时求值的副本,与 x 解耦。第二个例子:把 unique_ptr 移入闭包,闭包独占所有权——这在 C++11 做不到([ptr] 只能拷贝,unique_ptr 不可拷贝)。这是异步回调捕获资源的安全模式。

函数返回类型推导

C++11 引入了 auto f() -> TrailingType 的尾置返回类型,但仍需手写返回类型。C++14 允许直接 auto f() { return ...; },由 return 语句推导返回类型——类似 Lambda 的行为。

这对手写复杂返回类型(如迭代器类型、表达式模板类型)很省事。但有限制:所有 return 必须推导出同一类型;递归调用要求返回类型能从某条非递归 return 推导出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C++11 必须指定返回类型
auto add1(int x, int y) -> int {
return x + y;
}

// C++14 可以推导返回类型
auto add2(int x, int y) {
return x + y;
}

// 递归函数需要指定返回类型
auto factorial(int n) -> int {
return n <= 1 ? 1 : n * factorial(n - 1);
}

注意:返回类型推导会剥离引用与 const——若想返回引用需写 decltype(auto)(C++14)。decltype(auto) 保留精确类型(含引用/const),适合转发场景。可读性上,公共 API 显式返回类型有时更清晰,推导适合内部实现和类型复杂处。

变量模板

变量模板把"参数化的常量"做成模板,按类型实例化出对应的值。典型用途是类型相关的数学常量(pi<float> vs pi<double>)、类型属性查询。它让常量能随类型参数化,比写一堆 pi_float/pi_double 重载或函数更优雅。

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
constexpr T pi = T(3.1415926535897932385);

double d = pi<double>; // 3.1415926535897932385
float f = pi<float>; // 3.1415927f

// 其他变量模板
template<typename T>
constexpr std::size_t byte_count = sizeof(T);

std::size_t size = byte_count<int>; // 4

变量模板是 C++14 给元编程补的一块拼图,与类型别名(using)、函数模板配合,让编译期计算的表达更自然。它也是 std::numeric_limits 那类接口向更简洁形式演化的基础。

二进制字面量

C++14 引入 0b/0B 前缀的二进制字面量,取代此前只能用十六进制或位运算拼接表示二进制的别扭写法。对位掩码、硬件寄存器、协议位域等场景,二进制字面量让"哪一位是 1"一目了然。

1
2
3
4
5
6
int x = 0b1010;  // 10
int y = 0b11111111; // 255
int z = 0b1100'0101; // 197

// 无符号
unsigned int flags = 0b0000'1111'0000'0000;

配合数字分隔符 ',长二进制掩码可按字节/半字节分组,可读性远超十六进制。编译期常量位运算(如特征标志位定义)尤其受益。

数字分隔符

单引号 ' 作为数字分隔符,在字面量中插入不影响数值的视觉分组符。它解决了大数字难读的问题——1000000 是一百万还是十万要数零,1'000'000 一眼可辨。

1
2
3
4
5
6
7
int million = 1'000'000;
double pi = 3.14159'26535;
int binary = 0b1010'0101;
long long credit = 1'000'000'000'000LL;

// 十六进制
uint32_t mac = 0xDE'AD'BE'EF;

分隔符可用于十进制、十六进制、二进制、浮点,位置自由(编译器不限制每几位一组)。按惯例按千分位、字节边界(hex 每两位)、半字节(binary 每四位)分组。纯语法糖,零运行期成本。

[[deprecated]] 属性

[[deprecated]] 标记某实体已废弃,使用时会触发编译警告(可附消息说明替代方案)。它让"逐步淘汰旧 API"有编译期反馈——比注释或文档更可靠,因为编译器会主动提醒每个使用点。

1
2
3
4
5
[[deprecated("Use newFunction instead")]]
void oldFunction() {}

// 使用
[[deprecated]] int deprecatedVar = 42;

适用于类、函数、变量、typedef、枚举等。迁移旧代码时:先 [[deprecated]] 标记、警告暴露所有使用点、逐个替换、最后删除。[[deprecated("reason")]] 的消息会出现在警告里,指引使用者转向新 API。

放宽 constexpr 限制

C++11 的 constexpr 函数体只能有一条 return,迫使复杂逻辑用递归三元实现,可读性极差。C++14 大幅放宽:允许局部变量、循环(for/while)、if/switch、多 return、修改局部变量。这让编译期计算能像普通函数一样写。

1
2
3
4
5
6
7
8
9
10
// C++14 constexpr 函数可以有局部变量和循环
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}

constexpr int f = factorial(5); // 120

仍有限制:不能有 goto、不能定义非 constexpr 的局部变量、不能有 try/catch、不能修改全局状态。但日常编译期计算(查表、递推、字符串处理)已基本无障碍。这是让 constexpr 从"玩具"走向"实用"的关键一步。

std::make_unique

C++11 有 make_shared 却漏了 make_unique,是个明显的缺口——只能 unique_ptr<T>(new T(...))new。C++14 补上了 make_unique,与 make_shared 对称,提供异常安全和一致的风格。

1
2
3
4
5
6
7
8
9
10
11
// C++11 没有 make_unique
std::unique_ptr<int> ptr1(new int(42));

// C++14 添加了 make_unique
std::unique_ptr<int> ptr2 = std::make_unique<int>(42);

// 数组
auto arr = std::make_unique<int[]>(10);

// 更安全:异常安全
auto smartPtr = std::make_unique<MyClass>(args...);

异常安全的意义:f(unique_ptr<T>(new T), unique_ptr<U>(new U)) 中若第一个 new T 成功、第二个 new U 抛异常,且编译器求值顺序先 new Tnew U,则 T 会泄漏。make_unique 把分配和构造包在一起,杜绝此类问题。优先 make_unique 而非裸 new

std::quoted

std::quoted 是个 I/O 操纵器,用于读写带引号的字符串——自动加引号、转义内部引号、读回时去引号。它解决了"输出 CSV/配置时手动处理引号转义"的繁琐与易错。

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

std::string input = R"(Hello "World"!)";
std::stringstream ss;
ss << std::quoted(input);
std::cout << ss.str() << "\n"; // 输出: "Hello \"World\"!"

// 读取
std::string output;
ss >> std::quoted(output); // output = Hello "World"!

写时:把字符串用引号包起来,内部的引号转义。读时:识别引号边界、还原转义、支持空格(普通 >> 遇空格停)。可自定义分隔符/转义符/引号字符。适合 CSV、配置文件、shell 风格参数处理。

std::exchange

std::exchange(obj, new_value)obj 替换为 new_value 并返回旧值——“原子地取出旧值并写入新值”。它替代了"先存旧值、再赋新值"的两步模式,在移动语义场景下尤其自然:取出资源并清空源对象。

1
2
3
4
5
6
7
8
9
10
11
12
// C++11 需要手动实现
template<typename T, typename U>
T exchange(T& obj, U&& newValue) {
T oldValue = std::move(obj);
obj = std::forward<U>(newValue);
return oldValue;
}

// C++14 直接使用
std::vector<int> v{1, 2, 3};
std::vector<int> old = std::exchange(v, {4, 5});
// old = {1, 2, 3}, v = {4, 5}

典型用途:移动构造/移动赋值里"取出成员并置空"、实现 pop 类操作(取值并清空)、状态机切换。exchange 默认对旧值用 std::move,避免拷贝——这是它比手写两步更优的关键。

std::integer_sequence

std::integer_sequence<T, Ints...> 是编译期整数序列,std::make_index_sequence<N> 生成 0,1,...,N-1。它是变参模板元编程的核心工具——把"数组/tuple 的大小"变成"可展开的索引包",从而能用索引访问 tuple 元素、展开参数包到固定元数函数。

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

// 编译时整数序列
template<std::size_t... Ints>
void printIndices(std::index_sequence<Ints...>) {
((std::cout << Ints << " "), ...);
}

printIndices(std::make_index_sequence<5>{}); // 0 1 2 3 4

// 应用到参数包
template<typename T, std::size_t... Is>
void printTupleElements(const T& t, std::index_sequence<Is...>) {
((std::cout << std::get<Is>(t) << " "), ...);
}

经典模式:定义一个接受 index_sequence<Is...> 的辅助函数,用 make_index_sequence<tuple_size> 调用它,在函数体内用 Is... 索引展开 tuple。C++17 的 std::apply、折叠表达式让很多场景不再需要手写这套样板,但理解 integer_sequence 仍是读懂标准库元编程的基础。

chrono 和 string 字面量

C++14 给 <chrono> 和字符串加了用户定义字面量:100ms2s1h 直接是 chrono::duration"hello"s 直接是 std::string。此前要写 std::chrono::milliseconds(100)std::string("hello"),冗长。

时间字面量让睡眠、超时、计时的代码自文档化——sleep_for(500ms)sleep_for(std::chrono::milliseconds(500)) 清晰得多。字符串字面量让 "a"s + "b" 能直接拼接(const char* 不能用 +)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using namespace std::chrono_literals;

// 时间字面量
auto duration = 100ms; // 100 毫秒
auto seconds = 2s; // 2 秒
auto minutes = 5min; // 5 分钟
auto hours = 1h; // 1 小时

// 组合使用
auto complexDuration = 1h + 30min + 45s;

// 字符串字面量(C++14)
auto str = "hello"s; // std::string
auto u8str = u8"world"s; // std::string (UTF-8)
auto wstr = L"wide"s; // std::wstring

时间字面量是强类型:1h + 30min 自动提升到共同单位,类型安全。注意 u8"..."s 在 C++20 起返回 std::u8string(C++14/17 返回 std::string),跨版本需留意。

std::shared_timed_mutex

std::shared_timed_mutex 是支持共享/独占两种模式且带超时能力的读写锁。多个读者可同时持有共享锁,写者独占。C++14 引入它是为了填补 C++11 只有 std::mutex(独占)的空白——读多写少的场景下,读写锁能显著提升并发度。

“timed” 表示支持 try_lock_for/try_lock_shared_for 等带超时的锁定。C++17 的 std::shared_mutex 是其不带超时的轻量版。

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

class ReaderWriter {
mutable std::shared_timed_mutex mtx_;
std::vector<int> data_;

public:
void write(int value) {
std::unique_lock<std::shared_timed_mutex> lock(mtx_);
data_.push_back(value);
}

std::vector<int> read() const {
std::shared_lock<std::shared_timed_mutex> lock(mtx_);
return data_;
}
};

权衡:读写锁比普通 mutex 开销大(维护读者计数、状态机),只有在读操作占主导、读操作较重(如遍历大容器)时才划算。读操作很短或写频繁时,普通 mutex 可能更快。C++17 起优先 shared_mutex(除非需要超时)。

std::shared_lock

std::shared_lockshared_mutex/shared_timed_mutex 共享模式的 RAII 包装器:构造时获取共享锁(读锁)、析构时释放。它对应 std::unique_lock 的独占模式。多个 shared_lock 可同时持有同一 shared_mutex,实现多读。

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

std::shared_timed_mutex mtx;

// 共享锁(读锁)
std::shared_lock<std::shared_timed_mutex> read_lock(mtx);
// 可以多个线程同时持有

// 独占锁(写锁)
std::unique_lock<std::shared_timed_mutex> write_lock(mtx);
// 独占访问

shared_lockunique_lock 的区别仅在于锁模式:前者获取共享锁(多读并发),后者获取独占锁(互斥写)。读路径用 shared_lock、写路径用 unique_lock 是读写锁的标准用法。shared_lock 也支持延迟锁定、转移所有权等 unique_lock 的功能。

常用特性总结

最常用的 C++14 特性:

  • 泛型 Lambda
  • 函数返回类型推导
  • std::make_unique
  • 数字分隔符
  • 二进制字面量