C++14 新特性
C++14 是一个 minor 版本,没有重量级新特性,主要是给 C++11 打补丁、消除使用中的不便:泛型 lambda、初始化捕获、make_unique、放松 constexpr 限制等。本文逐项说明这些「小而关键」的改进,每项附用法与原理。
编译器支持情况见 Compiler support for C++14。系列总览见 现代 C++ 新特性汇总。
语言特性
泛型 lambda
lambda 形参可以写 auto,相当于给闭包的 operator() 加了模板:
auto less_than = [](const auto& a, const auto& b) { return a < b; };
less_than(1, 2); // int
less_than("a", "b"); // const char*
less_than(1.0, 2); // 两个参数类型还可以不同
std::sort(v.begin(), v.end(), less_than); // 直接当通用比较器 原理:编译器把 auto 形参展开成模板形参,每个不同实参组合实例化一份 operator(),与手写模板仿函数等价。
lambda 捕获中使用表达式(初始化捕获)
捕获列表里可以「定义新变量」,用表达式初始化,这是 C++20 之前把只移对象搬进 lambda 的唯一办法:
auto widget = std::make_unique<Widget>(args);
// C++11 做不到:unique_ptr 不能拷贝,也没法按值捕获
auto task = [w = std::move(widget)]() { // w 是闭包的新成员,由移动构造
w->run();
};
// 也可以引入任意计算出来的捕获变量
auto counter = [n = 0]() mutable { return ++n; }; // 自带状态的闭包
counter(); counter(); // 返回 1, 2 原理:名字 = 表达式 在闭包类型里生成一个对应类型的成员变量,用表达式初始化——这就是 C++20 之前实现「移动捕获」的标准手法。
函数返回类型推导
函数返回类型可以省略,交给编译器从 return 语句推导:
auto add(int a, int b) { return a + b; } // 返回 int
template<typename T, typename U>
auto pick(bool flag, T a, U b) { // 模板里终于不用写尾置类型
return flag ? a : b; // 多个 return 必须推导出同一类型
} 限制:多条 return 语句类型必须一致;递归调用要求前置 return 已出现;推导结果默认是值(丢弃引用),这正是引出 decltype(auto) 的原因。
decltype(auto)
另一种类型推导:用 decltype 的规则(保留引用和 const)而不是 auto 的规则(丢弃):
std::vector<int> v{1, 2, 3};
auto a = v[0]; // int:operator[] 返回 int&,auto 丢引用 -> 拷贝了一份
decltype(auto) b = v[0]; // int&:保留引用语义
// 典型场景:转发访问器,保持容器元素的原有引用性
template<typename C>
decltype(auto) at(C& c, std::size_t i) { return c[i]; }
// at(v, 0) = 42; 只有 decltype(auto) 版本能改到 v[0] 注意陷阱:绝不能 decltype(auto) 返回局部变量的引用(悬垂引用)。
放松的 constexpr 限制
constexpr 函数体里允许局部变量、循环、分支,不再限制为单条 return:
constexpr int factorial(int n) {
int r = 1;
for (int i = 2; i <= n; ++i) // 循环 OK
r *= i; // 可变局部变量 OK
return r;
}
static_assert(factorial(5) == 120, "");
constexpr int sum_to(int n) {
if (n < 1) return 0; // 分支 OK
return n * (n + 1) / 2;
} 原则没变:编译期求值仍然要求所有路径可确定、无副作用逃逸,放松的只是书写形式,从此 constexpr 函数可以写得和普通函数一样。
变量模板
模板化的变量,为「每个类型一份常量」提供统一写法:
template<typename T>
constexpr T pi = T(3.1415926535897932385L);
double d = pi<double>; // double 精度
float f = pi<float>; // float 精度 标准库自己用它实现了元函数的值版本(如 C++17 的 std::is_integral_v 一族 _v 变量模板)。
聚合类成员初始化
有默认成员初始化器的类仍然是聚合,可以继续用聚合初始化:
struct S {
int i;
int j = 42; // C++14:带默认值也还是聚合
};
S s{1}; // i == 1,j 取默认值 42(C++11 中这直接编译错误) 二进制字面量
0b / 0B 前缀直接写二进制,位掩码不再需要换算:
unsigned mask_a = 0b0000'0001; // 位 0(分位符也是 C++14,见下)
unsigned mask_b = 0b0100'0000; // 位 6
unsigned flags = 0b1010'1010; 数字分位符
单引号 ' 作为数字分隔符,纯粹提高可读性,编译期被丢弃:
long budget = 1'000'000'000;
double tiny = 0.000'000'001;
int hex_mask = 0xFF'FF'FF'FF;
int bin = 0b1011'0011'0001; // 与二进制字面量配合使用最为直观 deprecated 属性
[[deprecated]] 标记「还能用但不建议用」,调用处产生编译告警:
[[deprecated("use new_api() instead")]]
void old_api();
[[deprecated]] // 不带理由也行
int legacy_counter(); 告警随使用点出现,比注释 "DEPRECATED" 有效得多。
标准库特性
共享的互斥体和锁:shared_timed_mutex
读写锁:读操作共享、写操作独占,读多写少场景提升并发度:
std::shared_timed_mutex rw;
{ // 读端:多个线程可同时持有 shared 锁
std::shared_lock<std::shared_timed_mutex> rl(rw);
serve_snapshot();
}
{ // 写端:独占
std::unique_lock<std::shared_timed_mutex> wl(rw);
mutate();
}
// "timed" 指带 try_lock_for 等超时接口;C++17 补了不带定时的更轻的 shared_mutex 元函数的别名
给 <type_traits> 里的元函数加上 _t 后缀的类型别名,省掉 typename ...::type:
// C++11 写法:
typename std::remove_reference<T>::type x;
// C++14 写法:
std::remove_reference_t<T> y;
using Value = std::remove_reference_t<int&>; // int
static_assert(std::is_same<Value, int>::value, ""); 一族别名:remove_const_t、decay_t、conditional_t、enable_if_t、result_of_t 等。C++17 又给 _value 类 trait 加了 _v 变量模板版本。
关联容器中的异构查找
map/set 支持用「可比较的其他类型」做 key 查找,不用构造临时 std::string:
// 需要透明比较器(无模板参数的 less<>)
std::map<std::string, int, std::less<>> m;
m.find("hello"); // const char* 直接查找,不构造临时 string
m.find(std::string_view{"hello"});// string_view 也行(C++17 起更常用) 原理:透明比较器内含 is_transparent 类型标记,容器据此启用模板化的 find<K> 重载,用 operator< 互比即可。string_view(C++17)让这个特性的实用性大增。
标准自定义字面量
标准库自带的字面量后缀,统一放在 std::literals 命名空间:
using namespace std::literals;
auto s = "hello"s; // std::string(而不是 const char*)
auto sv = R"(raw)"s + "!"s; // string 的全套运算符立即可用
auto d = 1h + 30min + 5s + 100ms;// std::chrono::duration,编译期检查单位 最实用的是时间字面量:std::this_thread::sleep_for(200ms)。
通过类型寻址多元组
std::get 可以用类型而不是下标取 tuple 元素(要求该类型在 tuple 中唯一):
std::tuple<int, double, std::string> t{1, 2.5, "three"};
auto& s = std::get<std::string>(t); // 按类型取,不怕改了元素顺序
auto& d = std::get<double>(t);
// std::get<int>(t) 若有两个 int -> 编译错误(必须唯一) 较小的标准库特性
std::make_unique:补上
make_shared的另一半,unique_ptr也有了安全的工厂函数:cppauto p = std::make_unique<Widget>(args); // 例外安全:C++17 前参数求值顺序未定 // f(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B)); // 若第二个 new 先成功、后面的抛异常,A 就泄漏了;make_unique 不会 f(std::make_unique<A>(), std::make_unique<B>());std::is_final、std::is_null_pointer 等新 trait
std::integer_sequence:编译期整数序列,配合变参模板解包,是
std::apply、tuple展开的实现基础cpptemplate<typename T, T... Ints> void check(std::integer_sequence<T, Ints...>) { // Ints... 在此处可展开为 0, 1, 2, ... } check(std::make_integer_sequence<int, 3>{});std::exchange:把变量换成新值、返回旧值(移动语义惯用件,
swap内部就用它)std::quoted:流读写带引号的字符串
小结
C++14 的价值在于「补齐 C++11 的欠账」:初始化捕获让 lambda 真正能接管只移资源,放松的 constexpr 让编译期计算可以按普通写法表达,make_unique 填上了智能指针工厂的最后一块缺口。升级成本几乎为零(主流编译器的 C++14 支持早已完备),没有理由停留在 C++11 模式。
系列其他篇目:现代 C++ 新特性汇总 · C++11 新特性 · C++17 新特性 · C++20 新特性 · C++23 新特性