这篇笔记主要写一下 C++14 的新特性。相比 C++11 那次大改,C++14 是一次小版本更新:它没有引入新的范式,而是把 C++11 里做到一半的东西补齐。也正因为如此,它的特性大多没什么学习成本,但用起来非常舒服。
函数返回类型推导
C++11 里,要推导返回类型必须写尾置返回类型加 decltype:
C++// C++11
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { return t + u; }
C++14 可以直接让编译器推导:
C++// C++14
template <typename T, typename U>
auto add(T t, U u) { return t + u; }
推导规则和 auto 变量一致——会丢弃引用和顶层 const。要保留它们,用 decltype(auto):
C++std::vector<int> v{1, 2, 3};
auto get1(std::vector<int>& c) { return c[0]; } // 返回 int,是一份拷贝
decltype(auto) get2(std::vector<int>& c) { return c[0]; } // 返回 int&
这个区别在写泛型的转发包装时很关键。
注意:返回类型推导要求函数定义对调用点可见,所以它不能用在只有声明的头文件接口上。
泛型 Lambda
C++11 的 Lambda 参数必须写明类型。C++14 允许用 auto,等价于生成一个带模板 operator() 的闭包类型:
C++auto print = [](const auto& x) { std::cout << x << '\n'; };
print(42);
print("hello");
print(3.14);
这让 Lambda 真正可以拿来写通用算法。配合标准库尤其明显:
C++std::sort(v.begin(), v.end(),
[](const auto& a, const auto& b) { return a.score > b.score; });
以前这里得写出完整的元素类型,容器一改就要跟着改。
Lambda 初始化捕获
C++11 的捕获列表只能捕获已有的变量,而且只能按值或按引用。这意味着只能移动的类型无法被捕获——std::unique_ptr 放不进 Lambda。
C++14 允许在捕获列表里直接初始化一个新变量:
C++auto ptr = std::make_unique<Widget>();
auto task = [p = std::move(ptr)]() { p->run(); }; // 所有权转移进闭包
也可以用来捕获一个表达式的结果,避免在闭包里重复计算:
C++auto f = [size = v.size() * 2]() { return size; };
std::make_unique
C++11 给了 make_shared 却漏了 make_unique,C++14 补上了。
C++auto p = std::make_unique<Widget>(arg1, arg2);
除了少写一次类型名,它还解决一个实际的安全问题。考虑:
C++process(std::unique_ptr<Widget>(new Widget), compute());
C++17 之前,函数参数的求值顺序是未指定的。编译器可以先 new Widget,再调用 compute(),最后才构造 unique_ptr。如果 compute() 抛异常,那个裸 new 出来的 Widget 就永远泄漏了。make_unique 把分配和接管放进同一个函数调用,这个窗口就不存在了。
规则:不要写裸 new。 有了 make_unique 和 make_shared,几乎没有必要再写。
变量模板
C++11 有函数模板和类模板,C++14 补上了变量模板:
C++template <typename T>
constexpr T pi = T(3.1415926535897932385);
float f = pi<float>;
double d = pi<double>;
标准库的 std::is_integral_v<T>、std::is_same_v<A, B> 这类 _v 后缀的类型特征,就是用变量模板实现的(C++17 引入),比 std::is_integral<T>::value 短得多。
放宽的 constexpr
C++11 的 constexpr 函数几乎只能有一条 return 语句,写任何有循环的东西都要靠递归:
C++// C++11:只能递归
constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }
C++14 允许 constexpr 函数里有局部变量、循环和分支:
C++// C++14
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
这让编译期计算从"炫技"变成了实用工具。查找表、位掩码、字符串哈希都可以在编译期算好。
其它小改动
二进制字面量与数字分隔符。
C++int mask = 0b1010'1010;
int large = 1'000'000;
分隔符 ' 可以放在任意位置,只影响可读性,不影响数值。写位掩码和大常数时很有用。
[[deprecated]] 属性。 标记即将废弃的接口,使用处会产生编译警告。
C++[[deprecated("use renderV2() instead")]]
void render();
std::exchange。 把新值写入并返回旧值,写移动构造函数时很顺手:
C++Buffer(Buffer&& o) noexcept
: data_(std::exchange(o.data_, nullptr)),
size_(std::exchange(o.size_, 0)) {}
共享互斥量 std::shared_timed_mutex。 读写锁进入标准库,允许多个读者或一个写者。
小结
C++14 里没有一个特性是必须停下来学的,但它们合在一起让 C++11 的代码明显变短:泛型 Lambda 让算法变通用,初始化捕获让所有权能进闭包,放宽的 constexpr 让编译期计算变得能写。如果你的项目还在 C++11,升到 C++14 几乎没有迁移成本,是性价比最高的一次升级。