C++14 新特性

紧随 C++11 推出的 C++14 标准定位非常明确,主要是在 C++11 的基础上做了一次细节完善与查漏补缺。

它没有引入庞大复杂的新机制,而是把 C++11 中一些限制偏严或遗漏的地方改得更加简单好用。掌握了前面的 C++11 语法之后,小伙伴们学习这一版新特性会非常轻松。

C++14 新特性概览

在 C++14 中,标准不仅放宽了 auto 和 constexpr 的使用限制,还补上了二进制字面量、数字分隔符以及 std::make_unique() 等实用工具。常用的代表性特性如下表所示。

C++14 常用特性
代表特性 所属类别 / 头文件 主要变化与作用
泛型 Lambda 语言特性 Lambda 形参支持写成 auto,同一个匿名函数可适配多种参数类型
函数返回类型推导 语言特性 普通函数支持将返回类型写为 auto,由编译器根据 return 语句推导
二进制字面量 语言特性 支持使用 0b 或 0B 前缀直接按二进制位书写整数
数字分隔符 语言特性 支持在数字中间插入单引号分组(如 1'000'000),方便分位阅读
变量模板 语言特性 除函数模板和类模板外,支持直接定义带模板参数的变量
constexpr 限制放宽 语言特性 constexpr 函数内部支持定义局部变量、编写 if 分支和循环语句
std::make_unique() <memory> 补齐创建 unique_ptr 的工厂函数,省去手动编写 new

auto 类型推导的扩展

在 C++11 中,auto 关键字主要用于声明变量时自动推导类型。到了 C++14,auto 的使用范围进一步扩展到了 Lambda 参数列表和普通函数的返回类型上。

1. 泛型 Lambda

在 C++11 中编写 Lambda 表达式时,形参必须写出具体类型,比如 [](int a, int b)。从 C++14 开始,我们可以直接把形参写成 [](auto a, auto b),让编译器根据传入的实参自动推导类型,效果就像函数模板一样。下面我们来看一个例子。

示例 1:泛型 Lambda 适配多种类型

#include <iostream>
#include <string>

int main() {
    // 形参使用 auto,自动适配不同类型
    auto add = [](auto a, auto b) {
        return a + b;
    };

    std::string s1 = "Hello, ";
    std::string s2 = "C++14";

    std::cout << "整数相加:" << add(10, 20) << '\n';
    std::cout << "小数相加:" << add(1.5, 2.3) << '\n';
    std::cout << "文本拼接:" << add(s1, s2) << '\n';
    return 0;
}

运行结果如下。

整数相加:30
小数相加:3.8
文本拼接:Hello, C++14

分析:

在这个例子中,泛型 Lambda 表达式 add 的两个形参都写成了 auto。当我们分别传入 int、double 和 std::string 时,编译器会自动推导出对应的参数类型。这样同一份代码既能算整数和小数加法,又能拼接字符串,省去了编写多个重载函数的麻烦。

2. 函数返回类型推导

在定义普通函数时,如果我们不想手动写出函数返回值类型,也可以直接写成 auto,交由编译器根据函数体内的 return 语句自动推导。

示例 2:推导普通函数的返回类型

#include <iostream>

// 自动推导返回类型
auto square(int n) {
    return n * n;    // 推导为 int
}

int main() {
    std::cout << "square(6) = " << square(6) << '\n';
    return 0;
}

运行结果如下。

square(6) = 36

分析:

在 auto square(int n) 中,由于参数 n 是 int 类型,编译器会根据 return n * n; 自动将返回类型推导为 int。

需要注意的是,如果函数内部包含多条 return 语句,所有分支返回的数据类型必须一致,否则编译时会直接报错。下面我们对比一下正确写法与错误写法。

// 正确方式
auto getValue(bool flag) {
    if (flag) {
        return 1.0;
    }
    return 2.5;
}

// 错误方式
auto getBadValue(bool flag) {
    if (flag) {
        return 1;
    }
    return 2.5;
}

为什么下面的写法会报错呢?因为在 getBadValue() 中,第 1 个分支返回的是 int 类型的 1,而后面返回的却是 double 类型的 2.5。编译器无法确定唯一的返回类型,因此无法通过编译。

二进制字面量与数字分隔符

在书写数字字面量时,C++14 提供了两个非常直观的小改进:

  • 二进制字面量(0b / 0B):新增了 0b 或 0B 前缀,我们可以直接按二进制位写出整数。比如 0b1010 就表示十进制的 10,在做位运算或设置状态标志时一目了然。
  • 数字分隔符(单引号 '):遇到位数较长的数字时,肉眼很容易数错零的个数。我们可以在数字中间插入英文单引号(')分组,比如写成 1'000'000 或 0b0000'1111。编译时会自动忽略这些单引号,不影响数值大小。

我们来看一个简单例子就懂了。

示例 3:二进制字面量与数字分隔符

#include <iostream>

int main() {
    int binVal = 0b1010;          // 二进制 1010
    int lowMask = 0b0000'1111;    // 按 4 位分组
    int million = 1'000'000;      // 按千位分组

    std::cout << "binVal = " << binVal << '\n';
    std::cout << "lowMask = " << lowMask << '\n';
    std::cout << "million = " << million << '\n';
    return 0;
}

运行结果如下。

binVal = 10
lowMask = 15
million = 1000000

分析:

int binVal = 0b1010; 表示使用 0b 前缀直接定义二进制整数 1010,换算成十进制就是 10。下方的 0b0000'1111 换算成十进制是 15,它和 1'000'000 里面的单引号都只起视觉分组作用,打印结果与不加单引号完全相同。

单引号分隔符既可以每 3 位分一组,也可以像二进制那样每 4 位分一组。不过它只能放在两个数字之间,不能放在开头、末尾或紧挨在 0b 前缀后面,写成 0b'1010 就会编译报错。以后小伙伴们看到数字中间夹着单引号,可别误以为是语法写错了。

变量模板

前面我们已经学过函数模板和类模板,从 C++14 开始,我们还可以直接定义 “变量模板”。它非常适合用来定义需要随数据类型自动调整精度的常量,比如圆周率 pi<double> 和 pi<float>。

下面我们定义一个圆周率变量模板 pi<T>。为了看清不同浮点类型的精度差异,示例中引入了 <iomanip> 头文件,将小数统一显示为 10 位。

示例 4:定义并使用变量模板

#include <iomanip>    // 提供 fixed 与 setprecision()
#include <iostream>

// 定义圆周率变量模板
template <typename T>
constexpr T pi = static_cast<T>(3.141592653589793);

int main() {
    std::cout << std::fixed << std::setprecision(10);
    std::cout << "pi<float> = " << pi<float> << '\n';
    std::cout << "pi<double> = " << pi<double> << '\n';
    std::cout << "pi<int> = " << pi<int> << '\n';
    return 0;
}

运行结果如下。

pi<float> = 3.1415927410
pi<double> = 3.1415926536
pi<int> = 3

分析:

在这个例子中,template <typename T> constexpr T pi 定义了一个带类型参数 T 的常量模板,并通过 static_cast<T> 在编译期将圆周率转换为尖括号里指定的目标类型。

从运行结果可以看出,pi<float> 受限于单精度 float 只有 6~7 位有效数字,打印到第 10 位小数时末尾出现了舍入误差。pi<double> 拥有 15~17 位有效数字,前 10 位小数完全准确,而 pi<int> 则直接截断小数得到整数 3。

这里顺便提一点:标准库在后续版本中提供的各类 _v 类型判断常量(例如判断是否为整数类型的 std::is_integral_v<T>),底层其实就是使用变量模板定义出来的。

constexpr 函数限制放宽

在 C++11 刚引入 constexpr 时,编译期函数的语法限制非常严格:函数体内不能定义普通局部变量,也不能编写 if 分支或循环语句,通常只能写一条简单的 return 语句。

C++14 大幅放宽了这些限制。我们可以在 constexpr 函数内部正常定义局部变量、使用 if...else 判断以及 for 循环,写起来就跟普通函数一样直观。请看下面例子。

示例 5:constexpr 函数中使用循环

#include <iostream>

// 编译期计算 1 到 n 的累加和
constexpr int sumTo(int n) {
    int total = 0;
    for (int i = 1; i <= n; i++) {
        total += i;
    }
    return total;
}

int main() {
    constexpr int res = sumTo(10);
    std::cout << "累加结果:" << res << '\n';
    return 0;
}

运行结果如下。

累加结果:55

分析:

在上面例子中,constexpr int res = sumTo(10); 要求编译器在编译阶段直接算出 1 到 10 的累加和。放宽限制后,sumTo() 内部可以直接定义局部变量 total 并配合 for 循环累加,不用再像 C++11 那样绕弯编写递归函数了。

std::make_unique() 工厂函数

在智能指针方面,C++11 虽然同时引入了 std::unique_ptr 和 std::shared_ptr,但当时只提供了 std::make_shared(),却漏掉了配套的 std::make_unique()。

C++14 在 <memory> 头文件中正式补齐了 std::make_unique() 工厂函数。这样我们在创建独占智能指针时,就不再需要手动写出 new 了。

示例 6:创建 unique_ptr 智能指针

#include <iostream>
#include <memory>

int main() {
    // C++14 推荐写法
    auto p1 = std::make_unique<int>(100);

    // C++11 旧写法
    std::unique_ptr<int> p2(new int(100));

    std::cout << "*p1 = " << *p1 << '\n';
    std::cout << "*p2 = " << *p2 << '\n';
    return 0;
}

运行结果如下。

*p1 = 100
*p2 = 100

分析:

auto p1 = std::make_unique<int>(100); 表示在堆内存中创建一个初值为 100 的 int 对象,并返回管理它的独占智能指针。在实际开发中,我们更推荐使用 std::make_unique(),配合 auto 不用把类型名重复写两遍,代码也更安全整洁。

注意 小伙伴们在维护老项目时要留意版本差异:std::unique_ptr 是 C++11 引入的,而 std::make_unique() 是 C++14 才补上的。如果旧项目的编译参数锁定为 -std=c++11,调用 std::make_unique() 就会报错提示找不到该函数。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号