跨越十年的C++演进系列,分为5篇,本文为第二篇,后续会持续更新C++17、C++20、C++23~
第一篇:C++11新特性:跨越十年的C++演进:C++11新特性全解析
C++14是 C++ 编程语言的又一次重要更新,于 2014 年正式发布。它是对 C++11 的一次小幅但非常实用的增强,延续了现代 C++ 的演进方向,在保持语言一致性的基础上进一步提升了开发效率和代码可读性。
跨越十年的C++演进系列,分为5篇,本文为第二篇,后续会持续更新C++17、C++20、C++23~
第一篇:C++11新特性:
跨越十年的C++演进:C++11新特性全解析
C++14是 C++ 编程语言的又一次重要更新,于 2014 年正式发布。它是对 C++11 的一次小幅但非常实用的增强,延续了现代 C++ 的演进方向,在保持语言一致性的基础上进一步提升了开发效率和代码可读性。
相较于 C++11,C++14 引入了约 20 多个新特性,并修复和完善了大量已有的语言设计问题。这些改进虽然整体上不如 C++11 那样具有革命性,但在实际开发中极大地增强了语言的表达能力和易用性。
C++14 被广泛认为是一个“成熟版”的现代 C++ 标准,它在工业界得到了快速普及,成为许多项目推荐使用的默认标准之一。
话不多说,开始聊聊C++14的新特性~
1、变量模板
变量模板是C++中一种实现泛型编程的机制,它允许定义一个模板化的变量,类似于函数模板可以根据不同的类型生成对应的函数,变量模板也可以根据类型参数生成特定类型的变量实例。
这种机制在编写需要处理多种数据类型的通用代码时非常有用。通过变量模板,可以避免为每种类型重复定义相同的变量,从而提高代码的复用性和可维护性。
示例代码:
#include <iostream>
template<typename T>
constexpr T pi = T(3.1415926535897932385);
int main() {
std::cout << "int类型的pi值: " << pi<int> << std::endl;
std::cout << "double类型的pi值: " << pi<double> << std::endl;
return 0;
}
在上述示例中,定义了一个变量模板 pi,它接受一个类型参数 T。根据传入的不同类型,该模板会生成相应类型的 pi 常量值。
在 main 函数中,分别使用了 pi<int> 和 pi<double> 来实例化整型和双精度浮点型的 pi 值。这种方式能够以统一的形式访问不同类型下的常量值,体现了变量模板在泛型编程中的灵活性与实用性。
2、泛型 Lambda 表达式
C++11 引入了 Lambda 表达式,极大地简化了匿名函数的定义和使用,使开发者能够在代码中就地书写简洁的函数对象。然而,在 C++11 中,Lambda 表达式的参数类型必须在编写时明确指定,这在处理多种类型的数据时显得不够灵活。
为了解决这一限制,C++14 对 Lambda 表达式进行了增强,引入了泛型 Lambda 表达式的支持。通过使用 auto 作为参数类型,编译器可以自动推导传入参数的实际类型,从而使得同一个 Lambda 表达式可以适用于不同的数据类型,具备了类似模板函数的通用性。
这种机制不仅提升了代码的复用率,还增强了程序的灵活性和可读性,尤其适用于需要对不同类型容器执行相同逻辑操作的场景。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 3, 2, 4};
std::vector<double> dvec = {1.1, 3.3, 2.2, 4.4};
// 使用泛型lambda表达式对整数向量进行排序
std::sort(vec.begin(), vec.end(), [](auto a, auto b) { return a < b; });
// 使用相同的泛型lambda表达式对双精度向量进行排序
std::sort(dvec.begin(), dvec.end(), [](auto a, auto b) { return a < b; });
// 输出排序结果
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
for (double num : dvec) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
在这个示例中,定义了一个泛型 Lambda 表达式:
[](auto a, auto b) { return a < b; }
这里的 auto 关键字允许编译器根据调用上下文自动推导出参数的实际类型。当该 Lambda 被用于排序 std::vector<int> 时,参数被推导为 int 类型;而用于 std::vector<double> 时,则被推导为 double 类型。
通过这种方式,可以使用同一段 Lambda 代码处理不同类型的输入数据,无需重复编写多个版本,显著提高了代码的通用性和简洁性。
3、返回类型推导的扩展
C++11 已经引入了函数返回类型自动推导的功能,允许使用 auto 关键字作为函数的返回类型,由编译器根据函数内部的返回语句自动推断实际返回类型。然而,在 C++11 中,这种推导机制存在一定的限制:例如,函数体中必须只包含一个 return 语句,不能包含复杂的控制流程。
C++14 对这一特性进行了增强和扩展,放宽了对函数结构的限制。现在,即使函数体内包含多个语句、条件分支或循环等复杂逻辑,只要所有返回路径都能明确推导出相同的类型,就可以使用 auto 来让编译器自动推断函数的返回类型。
这一改进显著提升了编写泛型代码和复杂函数时的灵活性与便捷性,使代码更简洁、易读,也减少了手动指定返回类型的繁琐工作。
示例代码:
#include <iostream>
// 根据条件返回不同值的函数,利用C++14扩展的返回类型推导
auto get_value(bool condition) {
if (condition) {
return 5; // 推导为 int 类型
} else {
return 3.14; // 推导为 double 类型
}
}
int main() {
std::cout << "返回int类型: " << get_value(true) << std::endl;
std::cout << "返回double类型: " << get_value(false) << std::endl;
return 0;
}
在上面的示例中,定义了一个函数 get_value,其返回类型被声明为 auto。该函数根据传入的布尔参数 condition 的值,返回不同的常量值:
- 当 condition 为 true 时,返回整数 5,此时编译器会将返回类型推导为 int;
- 当 condition 为 false 时,返回浮点数 3.14,此时返回类型被推导为 double。
尽管函数体中包含了条件分支和多个返回语句,C++14 的返回类型推导机制依然能够正确地识别每条路径上的返回类型,并确保类型一致性(如果返回类型不一致,编译器会报错)。
这使得可以在保证类型安全的前提下,以更自然的方式编写函数逻辑,而不必拘泥于函数结构的限制。
4、二进制字面量(Binary Literals)
在 C++14 之前,若想在代码中直接表示二进制数值,通常需要借助八进制、十六进制等格式,或者通过字符串转换的方式实现,这不仅不够直观,也容易出错,尤其是在进行位操作、底层硬件控制或处理特定二进制标志位时。
为了解决这一问题,C++14 引入了对二进制字面量的支持。可以直接使用前缀 0b 或 0B 来表示一个二进制数,极大地提升了代码的可读性和编写效率。
这种特性特别适用于以下场景:
- 位掩码(bitmask)操作
- 硬件寄存器配置
- 标志位(flag)设置
- 其他涉及二进制数据处理的任务
示例代码:
#include <iostream>
int main() {
int num = 0b1010; // 使用二进制字面量表示数字,对应十进制的10
std::cout << "二进制 1010 对应的十进制数: " << num << std::endl;
return 0;
}
使用了 0b1010 表示一个二进制数值。编译器会自动将其转换为对应的十进制整数 10。
这种方式能够以更自然的形式表达二进制数据,无需手动转换进制或依赖其他辅助函数,从而显著提高了代码的清晰度和开发效率。
例如,如果想设置某个寄存器的特定几位,可以这样写:
unsigned char flags = 0b10100000; // 设置第7位和第5位为1
相比使用十六进制 0xA0,二进制形式更便于理解每个位的具体含义。
5、数字分隔符
在编写代码时,我们经常会遇到非常长的整型或浮点型字面量,例如大额金融数值、科学计算中的高精度数据等。这类数字由于位数较多,在阅读和理解时容易出错,也不利于后期维护。
为了解决这一问题,C++14 引入了数字分隔符功能,允许使用单引号 ' 将数字按逻辑分组,从而显著提升其可读性。这种分隔符仅用于视觉上的辅助,不会对数值本身产生任何影响。
该特性尤其适用于以下场景:
- 金融领域的大金额表示
- 科学计算中的高精度常量
- 内存地址、寄存器值等底层开发任务
- 任何需要清晰表达多位数字的场合
示例代码:
#include <iostream>
int main() {
long long big_num = 123'456'789'012'345; // 使用分隔符提高可读性
double big_float = 1.234'567'89; // 浮点数同样支持数字分隔符
std::cout << "大整数: " << big_num << std::endl;
std::cout << "大浮点数: " << big_float << std::endl;
return 0;
}
使用了数字分隔符来格式化两个较长的数值:
- 123'456'789'012'345
- :将一个超长整数按照千分位的方式分隔,便于快速识别其数量级;
- 1.234'567'89
- :将浮点数的小数部分按三位一组分隔,有助于更直观地理解其精度结构。
这些分隔符可以放在数字的任意位置(但不能出现在开头或结尾),并且可以连续使用多个。例如,以下写法都是合法且有效的:
int x = 1'000'000; // 千分位风格
int y = 0b1010'1100; // 二进制位分组
int z = 0x1A'FF; // 十六进制分组
通过这种方式,可以在不影响程序行为的前提下,使数字更具可读性和逻辑性。
6、函数对象的改进:支持 constexpr的函数对象
在 C++14 中,对 constexpr 的支持得到了进一步扩展,不仅允许普通函数被标记为 constexpr,还允许用户自定义的函数对象(即重载了 operator() 的类实例)也能够声明为 constexpr。这意味着这些函数对象可以在编译期执行,并参与常量表达式的计算。
这一改进为开发者提供了更大的灵活性和更强的编译期计算能力,尤其适用于以下场景:
- 编译期常量计算
- 模板元编程(TMP)
- 高性能关键路径中的内联优化
- 在 constexpr 上下文中使用复杂的逻辑处理
通过将函数对象标记为 constexpr,可以在不牺牲可读性和封装性的前提下,实现高效的编译期求值。
示例代码:
#include <iostream>
class Add {
public:
constexpr int operator()(int a, int b) const {
return a + b;
}
};
int main() {
constexpr Add add_obj; // 声明一个 constexpr 函数对象
constexpr int result = add_obj(3, 5); // 在编译期完成加法运算
std::cout << "编译期计算结果: " << result << std::endl;
return 0;
}
在上述示例中,定义了一个名为 Add 的类,并重载了其 operator() 方法,使其成为一个函数对象。该方法被标记为 constexpr,表示它可以作为常量表达式的一部分,在编译阶段就被求值。
在 main 函数中:
- constexpr Add add_obj;
- 表示这是一个可在编译期使用的函数对象;
- constexpr int result = add_obj(3, 5);
- 则利用该函数对象在编译期完成了 3 + 5 的计算;
- 最终输出语句会在运行时直接打印出这个已在编译期确定的结果。
这种方式避免了运行时额外的计算开销,提高了程序效率,同时也保证了类型安全和良好的抽象能力。
实际应用价值
这种特性在现代 C++ 编程中具有重要意义,特别是在需要高性能和编译期验证的项目中,例如:
- 使用 constexpr 容器和算法进行编译期数据结构构建;
- 构建通用的编译期工具库;
- 提高模板元编程的可读性与可维护性。
7、聚合初始化的扩展
在 C++ 中,聚合初始化(Aggregate Initialization) 是一种简洁且直观的对象初始化方式,常用于数组、结构体等聚合类型。C++14 对其进行了增强,特别是在处理具有继承关系的聚合类时,放宽了原有的限制,使得开发者可以更自然地使用花括号 {} 初始化包含基类成员的派生类对象。
这一改进简化了代码书写,提高了可读性,并减少了对构造函数的依赖,尤其适用于需要快速初始化多个字段或嵌套结构的场景。
示例代码:
#include <iostream>
struct Base {
int base_num;
};
struct Derived : public Base {
int derived_num;
};
int main() {
// 使用扩展的聚合初始化方式初始化派生类对象
Derived d = {1, 2};
std::cout << "Base成员值: " << d.base_num
<< ", Derived成员值: " << d.derived_num << std::endl;
return 0;
}
在上述示例中,Derived类公有继承自 Base类,并新增了一个成员变量 derived_num。通过 C++14 支持的扩展聚合初始化语法:
Derived d = {1, 2};
我们可以直接使用花括号列表初始化该派生类对象:
- 第一个值 1 被用来初始化基类 Base 的成员 base_num;
- 第二个值 2 则用于初始化派生类 Derived 自身的成员 derived_num。
这种初始化顺序遵循 C++ 成员变量声明的顺序,包括基类成员先于派生类成员进行初始化。
实际优势
- 简洁直观
- :无需显式定义构造函数即可完成多层级对象的初始化;
- 提升可读性
- :初始化值与成员变量一一对应,逻辑清晰;
- 支持嵌套结构
- :可用于嵌套的聚合结构,如结构体数组、联合体等;
- 兼容旧有语法
- :保留了传统聚合初始化的风格,同时增强了功能。
8、放宽的 constexpr 函数限制
C++11 引入了 constexpr 函数机制,允许在编译期执行函数调用,从而生成常量表达式。然而,在 C++11 中,constexpr 函数的实现受到诸多限制,例如函数体中只能包含单一的返回语句、不能使用局部变量、循环、条件分支等复杂逻辑。
C++14 对这一特性进行了显著增强,放宽了对 constexpr 函数的语法限制,使其可以支持:
- 局部变量定义
- 多条语句和多个返回语句
- 简单的控制结构,如 if、for、while 等
这些改进使得开发者能够编写更为复杂的编译期计算逻辑,将更多原本只能在运行时完成的任务提前到编译阶段执行,从而提升程序性能、优化资源使用,并增强代码的通用性和类型安全性。
示例代码:
#include <iostream>
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
int main() {
constexpr int fact_5 = factorial(5); // 在编译期计算5的阶乘
std::cout << "5的阶乘: " << fact_5 << std::endl;
return 0;
}
在这个示例中,定义了一个 constexpr 函数 factorial,用于计算一个整数的阶乘。该函数包含了以下非 C++11 原生支持的结构:
- 局部变量 result
- for
- 循环控制结构
- 多个语句组成的函数体
在 C++14 及以后的标准中,这些都是合法且可以在编译期求值的。因此,当在 main 函数中使用constexprint fact_5 = factorial(5);时,编译器会在编译阶段就完成 5! 的计算,并将其结果 120 直接嵌入到程序中,避免了运行时开销。
实际应用价值
这种增强的 constexpr 功能具有广泛的实用价值,尤其适用于以下场景:
- 数学常量和公式计算
- :如阶乘、斐波那契数列、三角函数表等;
- 模板元编程替代方案
- :相比复杂的 TMP 技术,constexpr 提供了更易读、更可维护的编译期计算方式;
- 静态查找表构建
- :可在编译期预计算并存储常用数据;
- 断言与验证
- :在编译期验证某些逻辑条件,提高代码可靠性。
注意事项
尽管 C++14 放宽了限制,但 constexpr 函数仍需满足一些基本要求:
- 所有参数和返回类型必须是字面类型(literal type);
- 函数体中的操作必须能够在编译期确定;
- 如果函数无法在编译期求值,它仍然可以在运行时正常调用。
9、[[deprecated]]属性标记
C++14 引入了标准属性 [[deprecated]],用于标记那些已废弃但尚未移除的程序元素,例如类、变量、函数、枚举、成员函数等。当开发者在代码中使用了被 [[deprecated]] 修饰的内容时,编译器会在编译阶段发出警告,提示该内容可能在未来版本中被移除,建议避免继续使用。
这一特性有助于:
- 提高代码维护性:明确标识哪些接口已不推荐使用;
- 平滑过渡更新:为库作者提供一种优雅地弃用旧接口的方式;
- 避免潜在错误:提醒开发者尽早替换为新的替代方案。
此外,[[deprecated]] 是 C++ 标准的一部分,相比之前依赖于编译器扩展(如 GCC 的 __attribute__((deprecated)))的做法,它具有更好的可移植性和标准化支持。
示例代码:
#include <iostream>
struct [[deprecated]] A {
void foo() {
std::cout << "This is an old class." << std::endl;
}
};
int main() {
A a; // 使用了被废弃的类,编译时将产生警告
a.foo();
return 0;
}
使用 g++ 编译时(启用 C++14 标准):
g++ test.cpp -std=c++14 -Wall
输出结果如下:
test.cpp: In function ‘int main()’:
test.cpp:13:7: warning: ‘A’ is deprecated [-Wdeprecated-declarations]
A a;
^
test.cpp:6:23: note: declared here
struct [[deprecated]] A {
在这个示例中,结构体 A 被标记为 [[deprecated]]。当我们在 main() 函数中创建其对象 a 时,编译器会检测到这一使用行为,并生成相应的警告信息,提示开发者该类已被弃用。
下期预告:
C++17新特性
相较于 C++11,C++14 引入了约 20 多个新特性,并修复和完善了大量已有的语言设计问题。这些改进虽然整体上不如 C++11 那样具有革命性,但在实际开发中极大地增强了语言的表达能力和易用性。
C++14 被广泛认为是一个“成熟版”的现代 C++ 标准,它在工业界得到了快速普及,成为许多项目推荐使用的默认标准之一。
话不多说,开始聊聊C++14的新特性~
1、变量模板
变量模板是C++中一种实现泛型编程的机制,它允许定义一个模板化的变量,类似于函数模板可以根据不同的类型生成对应的函数,变量模板也可以根据类型参数生成特定类型的变量实例。
这种机制在编写需要处理多种数据类型的通用代码时非常有用。通过变量模板,可以避免为每种类型重复定义相同的变量,从而提高代码的复用性和可维护性。
示例代码:
#include <iostream>
template<typename T>
constexpr T pi = T(3.1415926535897932385);
int main() {
std::cout << "int类型的pi值: " << pi<int> << std::endl;
std::cout << "double类型的pi值: " << pi<double> << std::endl;
return 0;
}
在上述示例中,定义了一个变量模板 pi,它接受一个类型参数 T。根据传入的不同类型,该模板会生成相应类型的 pi 常量值。
在 main 函数中,分别使用了 pi<int> 和 pi<double> 来实例化整型和双精度浮点型的 pi 值。这种方式能够以统一的形式访问不同类型下的常量值,体现了变量模板在泛型编程中的灵活性与实用性。
2、泛型 Lambda 表达式
C++11 引入了 Lambda 表达式,极大地简化了匿名函数的定义和使用,使开发者能够在代码中就地书写简洁的函数对象。然而,在 C++11 中,Lambda 表达式的参数类型必须在编写时明确指定,这在处理多种类型的数据时显得不够灵活。
为了解决这一限制,C++14 对 Lambda 表达式进行了增强,引入了泛型 Lambda 表达式的支持。通过使用 auto 作为参数类型,编译器可以自动推导传入参数的实际类型,从而使得同一个 Lambda 表达式可以适用于不同的数据类型,具备了类似模板函数的通用性。
这种机制不仅提升了代码的复用率,还增强了程序的灵活性和可读性,尤其适用于需要对不同类型容器执行相同逻辑操作的场景。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 3, 2, 4};
std::vector<double> dvec = {1.1, 3.3, 2.2, 4.4};
// 使用泛型lambda表达式对整数向量进行排序
std::sort(vec.begin(), vec.end(), [](auto a, auto b) { return a < b; });
// 使用相同的泛型lambda表达式对双精度向量进行排序
std::sort(dvec.begin(), dvec.end(), [](auto a, auto b) { return a < b; });
// 输出排序结果
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
for (double num : dvec) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
在这个示例中,定义了一个泛型 Lambda 表达式:
[](auto a, auto b) { return a < b; }
这里的 auto 关键字允许编译器根据调用上下文自动推导出参数的实际类型。当该 Lambda 被用于排序 std::vector<int> 时,参数被推导为 int 类型;而用于 std::vector<double> 时,则被推导为 double 类型。
通过这种方式,可以使用同一段 Lambda 代码处理不同类型的输入数据,无需重复编写多个版本,显著提高了代码的通用性和简洁性。
3、返回类型推导的扩展
C++11 已经引入了函数返回类型自动推导的功能,允许使用 auto 关键字作为函数的返回类型,由编译器根据函数内部的返回语句自动推断实际返回类型。然而,在 C++11 中,这种推导机制存在一定的限制:例如,函数体中必须只包含一个 return 语句,不能包含复杂的控制流程。
C++14 对这一特性进行了增强和扩展,放宽了对函数结构的限制。现在,即使函数体内包含多个语句、条件分支或循环等复杂逻辑,只要所有返回路径都能明确推导出相同的类型,就可以使用 auto 来让编译器自动推断函数的返回类型。
这一改进显著提升了编写泛型代码和复杂函数时的灵活性与便捷性,使代码更简洁、易读,也减少了手动指定返回类型的繁琐工作。
示例代码:
#include <iostream>
// 根据条件返回不同值的函数,利用C++14扩展的返回类型推导
auto get_value(bool condition) {
if (condition) {
return 5; // 推导为 int 类型
} else {
return 3.14; // 推导为 double 类型
}
}
int main() {
std::cout << "返回int类型: " << get_value(true) << std::endl;
std::cout << "返回double类型: " << get_value(false) << std::endl;
return 0;
}
在上面的示例中,定义了一个函数 get_value,其返回类型被声明为 auto。该函数根据传入的布尔参数 condition 的值,返回不同的常量值:
- 当 condition 为 true 时,返回整数 5,此时编译器会将返回类型推导为 int;
- 当 condition 为 false 时,返回浮点数 3.14,此时返回类型被推导为 double。
尽管函数体中包含了条件分支和多个返回语句,C++14 的返回类型推导机制依然能够正确地识别每条路径上的返回类型,并确保类型一致性(如果返回类型不一致,编译器会报错)。
这使得可以在保证类型安全的前提下,以更自然的方式编写函数逻辑,而不必拘泥于函数结构的限制。
4、二进制字面量(Binary Literals)
在 C++14 之前,若想在代码中直接表示二进制数值,通常需要借助八进制、十六进制等格式,或者通过字符串转换的方式实现,这不仅不够直观,也容易出错,尤其是在进行位操作、底层硬件控制或处理特定二进制标志位时。
为了解决这一问题,C++14 引入了对二进制字面量的支持。可以直接使用前缀 0b 或 0B 来表示一个二进制数,极大地提升了代码的可读性和编写效率。
这种特性特别适用于以下场景:
- 位掩码(bitmask)操作
- 硬件寄存器配置
- 标志位(flag)设置
- 其他涉及二进制数据处理的任务
示例代码:
#include <iostream>
int main() {
int num = 0b1010; // 使用二进制字面量表示数字,对应十进制的10
std::cout << "二进制 1010 对应的十进制数: " << num << std::endl;
return 0;
}
使用了 0b1010 表示一个二进制数值。编译器会自动将其转换为对应的十进制整数 10。
这种方式能够以更自然的形式表达二进制数据,无需手动转换进制或依赖其他辅助函数,从而显著提高了代码的清晰度和开发效率。
例如,如果想设置某个寄存器的特定几位,可以这样写:
unsigned char flags = 0b10100000; // 设置第7位和第5位为1
相比使用十六进制 0xA0,二进制形式更便于理解每个位的具体含义。
5、数字分隔符
在编写代码时,我们经常会遇到非常长的整型或浮点型字面量,例如大额金融数值、科学计算中的高精度数据等。这类数字由于位数较多,在阅读和理解时容易出错,也不利于后期维护。
为了解决这一问题,C++14 引入了数字分隔符功能,允许使用单引号 ' 将数字按逻辑分组,从而显著提升其可读性。这种分隔符仅用于视觉上的辅助,不会对数值本身产生任何影响。
该特性尤其适用于以下场景:
- 金融领域的大金额表示
- 科学计算中的高精度常量
- 内存地址、寄存器值等底层开发任务
- 任何需要清晰表达多位数字的场合
示例代码:
#include <iostream>
int main() {
long long big_num = 123'456'789'012'345; // 使用分隔符提高可读性
double big_float = 1.234'567'89; // 浮点数同样支持数字分隔符
std::cout << "大整数: " << big_num << std::endl;
std::cout << "大浮点数: " << big_float << std::endl;
return 0;
}
使用了数字分隔符来格式化两个较长的数值:
- 123'456'789'012'345
- :将一个超长整数按照千分位的方式分隔,便于快速识别其数量级;
- 1.234'567'89
- :将浮点数的小数部分按三位一组分隔,有助于更直观地理解其精度结构。
这些分隔符可以放在数字的任意位置(但不能出现在开头或结尾),并且可以连续使用多个。例如,以下写法都是合法且有效的:
int x = 1'000'000; // 千分位风格
int y = 0b1010'1100; // 二进制位分组
int z = 0x1A'FF; // 十六进制分组
通过这种方式,可以在不影响程序行为的前提下,使数字更具可读性和逻辑性。
6、函数对象的改进:支持 constexpr的函数对象
在 C++14 中,对 constexpr 的支持得到了进一步扩展,不仅允许普通函数被标记为 constexpr,还允许用户自定义的函数对象(即重载了 operator() 的类实例)也能够声明为 constexpr。这意味着这些函数对象可以在编译期执行,并参与常量表达式的计算。
这一改进为开发者提供了更大的灵活性和更强的编译期计算能力,尤其适用于以下场景:
- 编译期常量计算
- 模板元编程(TMP)
- 高性能关键路径中的内联优化
- 在 constexpr 上下文中使用复杂的逻辑处理
通过将函数对象标记为 constexpr,可以在不牺牲可读性和封装性的前提下,实现高效的编译期求值。
示例代码:
#include <iostream>
class Add {
public:
constexpr int operator()(int a, int b) const {
return a + b;
}
};
int main() {
constexpr Add add_obj; // 声明一个 constexpr 函数对象
constexpr int result = add_obj(3, 5); // 在编译期完成加法运算
std::cout << "编译期计算结果: " << result << std::endl;
return 0;
}
在上述示例中,定义了一个名为 Add 的类,并重载了其 operator() 方法,使其成为一个函数对象。该方法被标记为 constexpr,表示它可以作为常量表达式的一部分,在编译阶段就被求值。
在 main 函数中:
- constexpr Add add_obj;
- 表示这是一个可在编译期使用的函数对象;
- constexpr int result = add_obj(3, 5);
- 则利用该函数对象在编译期完成了 3 + 5 的计算;
- 最终输出语句会在运行时直接打印出这个已在编译期确定的结果。
这种方式避免了运行时额外的计算开销,提高了程序效率,同时也保证了类型安全和良好的抽象能力。
实际应用价值
这种特性在现代 C++ 编程中具有重要意义,特别是在需要高性能和编译期验证的项目中,例如:
- 使用 constexpr 容器和算法进行编译期数据结构构建;
- 构建通用的编译期工具库;
- 提高模板元编程的可读性与可维护性。
7、聚合初始化的扩展
在 C++ 中,聚合初始化(Aggregate Initialization) 是一种简洁且直观的对象初始化方式,常用于数组、结构体等聚合类型。C++14 对其进行了增强,特别是在处理具有继承关系的聚合类时,放宽了原有的限制,使得开发者可以更自然地使用花括号 {} 初始化包含基类成员的派生类对象。
这一改进简化了代码书写,提高了可读性,并减少了对构造函数的依赖,尤其适用于需要快速初始化多个字段或嵌套结构的场景。
示例代码:
#include <iostream>
struct Base {
int base_num;
};
struct Derived : public Base {
int derived_num;
};
int main() {
// 使用扩展的聚合初始化方式初始化派生类对象
Derived d = {1, 2};
std::cout << "Base成员值: " << d.base_num
<< ", Derived成员值: " << d.derived_num << std::endl;
return 0;
}
在上述示例中,Derived类公有继承自 Base类,并新增了一个成员变量 derived_num。通过 C++14 支持的扩展聚合初始化语法:
Derived d = {1, 2};
我们可以直接使用花括号列表初始化该派生类对象:
- 第一个值 1 被用来初始化基类 Base 的成员 base_num;
- 第二个值 2 则用于初始化派生类 Derived 自身的成员 derived_num。
这种初始化顺序遵循 C++ 成员变量声明的顺序,包括基类成员先于派生类成员进行初始化。
实际优势
- 简洁直观
- :无需显式定义构造函数即可完成多层级对象的初始化;
- 提升可读性
- :初始化值与成员变量一一对应,逻辑清晰;
- 支持嵌套结构
- :可用于嵌套的聚合结构,如结构体数组、联合体等;
- 兼容旧有语法
- :保留了传统聚合初始化的风格,同时增强了功能。
8、放宽的 constexpr 函数限制
C++11 引入了 constexpr 函数机制,允许在编译期执行函数调用,从而生成常量表达式。然而,在 C++11 中,constexpr 函数的实现受到诸多限制,例如函数体中只能包含单一的返回语句、不能使用局部变量、循环、条件分支等复杂逻辑。
C++14 对这一特性进行了显著增强,放宽了对 constexpr 函数的语法限制,使其可以支持:
- 局部变量定义
- 多条语句和多个返回语句
- 简单的控制结构,如 if、for、while 等
这些改进使得开发者能够编写更为复杂的编译期计算逻辑,将更多原本只能在运行时完成的任务提前到编译阶段执行,从而提升程序性能、优化资源使用,并增强代码的通用性和类型安全性。
示例代码:
#include <iostream>
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
int main() {
constexpr int fact_5 = factorial(5); // 在编译期计算5的阶乘
std::cout << "5的阶乘: " << fact_5 << std::endl;
return 0;
}
在这个示例中,定义了一个 constexpr 函数 factorial,用于计算一个整数的阶乘。该函数包含了以下非 C++11 原生支持的结构:
- 局部变量 result
- for
- 循环控制结构
- 多个语句组成的函数体
在 C++14 及以后的标准中,这些都是合法且可以在编译期求值的。因此,当在 main 函数中使用constexprint fact_5 = factorial(5);时,编译器会在编译阶段就完成 5! 的计算,并将其结果 120 直接嵌入到程序中,避免了运行时开销。
实际应用价值
这种增强的 constexpr 功能具有广泛的实用价值,尤其适用于以下场景:
- 数学常量和公式计算
- :如阶乘、斐波那契数列、三角函数表等;
- 模板元编程替代方案
- :相比复杂的 TMP 技术,constexpr 提供了更易读、更可维护的编译期计算方式;
- 静态查找表构建
- :可在编译期预计算并存储常用数据;
- 断言与验证
- :在编译期验证某些逻辑条件,提高代码可靠性。
注意事项
尽管 C++14 放宽了限制,但 constexpr 函数仍需满足一些基本要求:
- 所有参数和返回类型必须是字面类型(literal type);
- 函数体中的操作必须能够在编译期确定;
- 如果函数无法在编译期求值,它仍然可以在运行时正常调用。
9、[[deprecated]]属性标记
C++14 引入了标准属性 [[deprecated]],用于标记那些已废弃但尚未移除的程序元素,例如类、变量、函数、枚举、成员函数等。当开发者在代码中使用了被 [[deprecated]] 修饰的内容时,编译器会在编译阶段发出警告,提示该内容可能在未来版本中被移除,建议避免继续使用。
这一特性有助于:
- 提高代码维护性:明确标识哪些接口已不推荐使用;
- 平滑过渡更新:为库作者提供一种优雅地弃用旧接口的方式;
- 避免潜在错误:提醒开发者尽早替换为新的替代方案。
此外,[[deprecated]] 是 C++ 标准的一部分,相比之前依赖于编译器扩展(如 GCC 的 __attribute__((deprecated)))的做法,它具有更好的可移植性和标准化支持。
示例代码:
#include <iostream>
struct [[deprecated]] A {
void foo() {
std::cout << "This is an old class." << std::endl;
}
};
int main() {
A a; // 使用了被废弃的类,编译时将产生警告
a.foo();
return 0;
}
使用 g++ 编译时(启用 C++14 标准):
g++ test.cpp -std=c++14 -Wall
输出结果如下:
test.cpp: In function ‘int main()’:
test.cpp:13:7: warning: ‘A’ is deprecated [-Wdeprecated-declarations]
A a;
^
test.cpp:6:23: note: declared here
struct [[deprecated]] A {
在这个示例中,结构体 A 被标记为 [[deprecated]]。当我们在 main() 函数中创建其对象 a 时,编译器会检测到这一使用行为,并生成相应的警告信息,提示开发者该类已被弃用。
下期预告:
C++17新特性
欢迎点击下方关注【Linux教程】,获取编程学习路线、原创项目教程、简历模板、面试题库、编程交流圈子。