目录
u8字符字面量
noexcept作为类型系统的一部分
Lambda表达式捕获*this
constexpr新特性
[编译期的if constexpr语句](#编译期的if constexpr语句)
constexpr的Lambda表达式
变量新特性
inline变量
结构化绑定
if和switch语句中的初始化器
强制的复制省略(返回值优化)
临时物质化
模板新特性
折叠表达式(...)
类模板实参推导
auto占位的非类型模板形参
u8字符字面量
C++17 引入了u8
字符字面量,用于表示 UTF-8 编码的字符串。
cpp
auto str = u8"Hello, 世界";
noexcept作为类型系统的一部分
noexcept
是 C++11 引入的一个关键字,用于改善C++中异常处理的性能和可用性。noexcept
指定的函数保证不会抛出异常,这使得编译器有机会进行优化,同时也为程序员提供了一个清晰的工具来指明哪些函数是安全的,即不会因为异常而失败。
C++17对其改动主要如下:
- 更广泛的使用
C++17 标准库在很多已有的函数中增加了noexcept
说明。这是因为对异常安全性有更高的要求和对性能优化的关注。比如,在移动语义和智能指针等方面,更频繁地看到noexcept
的使用。
- 推导规则
C++17 引入了新的推导指南,使得noexcept
能够在模板和自动类型推导中得到更好的处理。这包括在模板函数和自动返回类型中,noexcept
的状态可以被推导出来。例如,一个模板函数可以根据其模板参数的操作是否不抛出异常,来决定自身是否声明为noexcept
。
- 移动操作的默认noexcept
在 C++11 和 C++14 中,移动构造函数和移动赋值操作不会自动被推断为noexcept
,而在 C++17 中,如果一个类的所有成员和基类的移动构造函数和移动赋值操作都是noexcept
的,那么这个类的移动操作也会被推断为noexcept
。这改善了容器(如 std::vector)在元素类型是可移动但不抛出异常时的性能,因为容器可以安全地进行更优化的内存操作,如使用realloc
而不是手动复制。
- 对std::swap的优化
C++17 中,std::swap
在可能的情况下使用noexcept
来确保不抛出异常,这对于某些类型来说,特别是在模板编程中,可以提高效率和安全性。
Lambda表达式捕获*this
C++17的Lambda引入捕获*this
,使得可以捕获当前对象的常量副本,相当于是以值捕获的形式捕获了this
指向的对象,并且赋予const
属性。
cpp
class A
{
int a = 1;
public:
void printCopyA()
{
auto lambda = [*this] {
a++; // error C3490: 由于正在通过常量对象访问"a",因此无法对其进行修改
std::cout << a;
};
lambda();
}
void printA()
{
auto lambda = [&] {
a++;
std::cout << a;
};
lambda();
}
};
int main()
{
A a;
a.printA();
a.printCopyA();
}
constexpr新特性
编译期的if constexpr语句
C++17引入if constexpr
语句,这是一个在编译时决定条件分支的语言特性。主要是为了增强模板和编译时多态的功能,使得基于模板参数的条件编译变得更为直接和清晰。
cpp
#include <iostream>
#include <type_traits>
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integral type with value: " << value << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Floating-point type with value: " << value << std::endl;
} else {
std::cout << "Other type" << std::endl;
}
}
int main() {
process(10); // 输出:Integral type with value: 10
process(3.14); // 输出:Floating-point type with value: 3.14
process("Hello"); // 输出:Other type
}
特点和限制
- 编译时求值 :
if constexpr
的条件必须能在编译时得到求值。 - 作用域限制:只有符合条件的分支会被编译,这意味着在不符合条件的分支中的代码即使含有编译错误,也不会引起编译失败,因为这部分代码根本不会被编译。
例如下面代码,在
else
分支下调用不存在的函数,编译不会失败:
cpp
#include <iostream>
#include <type_traits>
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integral type with value: " << value << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Floating-point type with value: " << value << std::endl;
} else {
nonExistentFunc(value); // 不会被编译
std::cout << "Other type" << std::endl;
}
}
int main() {
process(10); // 输出:Integral type with value: 10
process(3.14); // 输出:Floating-point type with value: 3.14
}
constexpr的Lambda表达式
C++17 允许Lambda表达式在constexpr
上下文中使用,从而使其可以在编译期求值。
cpp
constexpr auto add = [](int a, int b) { return a + b; };
static_assert(add(2, 3) == 5);
变量新特性
inline变量
C++17 引入的内联变量是一个重要的语言特性,它主要解决了多个编译单元之间共享全局变量的链接问题,特别是对于模板和头文件中定义的变量。在此之前,全局变量或静态成员变量的定义可能会导致多个定义问题(One Definition Rule,ODR)的违规,尤其是在涉及到头文件中包含的变量时。
内联变量的用途
- 解决多个定义问题(ODR):在 C++ 中,非内联变量在多个源文件中定义时,会违反 ODR,导致链接错误。内联变量允许在多个编译单元中定义同一个变量,编译器保证在程序中只有一个实例。
- 简化模板和头文件中的变量定义 :C++17 之前,如果在头文件中定义静态成员变量或全局变量,通常需要在一个源文件中提供该变量的定义以避免链接时的重复定义问题。内联变量允许在头文件中直接定义并初始化变量,简化了代码结构。
语法
内联变量的声明非常直接,只需要在变量声明前加上 inline 关键字:
cpp
inline int myGlobalVar = 10;
结构化绑定
C++17 引入的结构化绑定(Structured Bindings)是一种新的语言特性,旨在提供一种简洁、直观的方式来解包(unpack)元组、结构体或数组中的数据到单独的变量中。这个特性极大地增强了代码的可读性和易用性,特别是在处理复合数据类型或从函数返回多个值时。
工作原理
结构化绑定允许你同时定义多个变量,将它们绑定到一个聚合数据类型(如元组、数组、结构体或配对)的各个成员上。这些变量可以被视作原始数据结构中对应成员的别名。
基本语法
结构化绑定的基本语法如下:
cpp
auto [x, y, z] = expression;
其中 expression 必须是一个返回聚合类型(如元组、结构体、数组)的表达式。x, y, z 等变量被创建为引用或值,这取决于表达式的类型和上下文。
使用场景
- 从函数返回多个值: 使用结构化绑定,函数可以返回一个 std::tuple 或 std::pair,调用者可以非常直观地获取这些值。
- 解包数组和元组: 直接将数组或元组的元素绑定到变量上,简化数组或元组的处理代码。
- 访问结构体成员 : 对于简单的 POD(Plain Old Data)类型的结构体,可以直接绑定到其成员上,而不需要逐一指定。
示例
- 元组解包
cpp
#include <tuple>
#include <iostream>
std::tuple<int, double, std::string> getTuple() {
return {42, 3.14, "Hello"};
}
int main() {
auto [a, b, c] = getTuple();
std::cout << a << ", " << b << ", " << c << std::endl; // 输出:42, 3.14, Hello
}
- 结构体解包
cpp
#include <iostream>
struct Point {
int x, y;
};
int main() {
Point p{10, 20};
auto [x, y] = p;
std::cout << x << ", " << y << std::endl; // 输出:10, 20
}
- 数组解包
cpp
#include <iostream>
int main() {
int arr[] = {1, 2, 3};
auto [a, b, c] = arr;
std::cout << a << ", " << b << ", " << c << std::endl; // 输出:1, 2, 3
}
if和switch语句中的初始化器
C++17引入if
和switch
语句的初始化器,使得在使用条件判断时可以在内部进行变量初始化。
if示例
cpp
#include <iostream>
#include <map>
int main() {
std::map<std::string, int> myMap = {{"Alice", 5}, {"Bob", 10}};
if (auto it = myMap.find("Alice"); it != myMap.end()) {
std::cout << "Found Alice with score " << it->second << std::endl;
} else {
std::cout << "Alice not found" << std::endl;
}
}
switch示例
cpp
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {10, 20, 30, 40};
switch (auto i = numbers.size(); i) {
case 4:
std::cout << "There are four elements." << std::endl;
break;
case 3:
std::cout << "There are three elements." << std::endl;
break;
default:
std::cout << "The number of elements is not 3 or 4." << std::endl;
}
}
优点
- 作用域控制:初始化的变量仅在 if 或 switch 语句块中有效,限制了变量的作用域,避免了不必要的作用域泄露。
- 代码清晰和紧凑:通过将相关的初始化和条件判断放在一起,代码更加整洁,逻辑更清晰。
- 避免前置声明 :不需要在 if 或 switch 前面单独声明变量,减少了代码行数和复杂性。
强制的复制省略(返回值优化)
C++17 引入的强制的复制省略(Guaranteed Copy Elision)或者更准确地称作返回值优化 (Return Value Optimization,RVO)的强化版本,是一个重要的编译器优化特性,它可以显著减少或消除某些情况下的对象复制和移动操作。这种优化不仅提高了程序的性能,还改善了资源管理,特别是在涉及到大型对象或者资源密集型对象时。
背景与问题
在 C++17 之前,返回局部对象时通常会涉及到复制或移动构造函数的调用,即使编译器应用了返回值优化(RVO)或命名返回值优化(NRVO)。但这些优化是可选的,不是强制的,这意味着编译器可以选择不进行这些优化,从而导致性能损失。
C++17 的改变
C++17 通过修改语言的核心规则(有时被称为"强制 RVO"或"保证复制省略"),确保在某些特定情况下消除这些复制和移动操作。这主要体现在两个方面:
- 当对象从函数返回时:C++17 要求编译器必须省略局部对象的复制和移动操作,直接在调用方的上下文中构造这些对象。
- 从表达式构造新对象时 :例如在使用列表初始化或直接初始化对象时。
技术细节
具体来说,C++17 标准规定,如果返回的对象类型与函数返回类型相符,并且返回的是一个局部对象或临时对象,编译器必须省略复制或移动构造函数的调用,直接在接收对象的内存位置构造返回对象。
示例
以下示例展示了 C++17 中的强制复制省略如何工作:
cpp
#include <iostream>
#include <vector>
class BigObject {
public:
std::vector<int> data;
BigObject() {
std::cout << "Constructor called" << std::endl;
}
BigObject(const BigObject&) {
std::cout << "Copy constructor called" << std::endl;
}
BigObject(BigObject&&) noexcept {
std::cout << "Move constructor called" << std::endl;
}
};
BigObject createBigObject() {
BigObject obj;
obj.data.resize(1000); // 假设是一个资源密集型操作
return obj;
}
int main() {
BigObject myObj = createBigObject();
// 应该看不到复制或移动构造函数的调用信息
return 0;
}
在 C++17 中运行这段代码时,你不会看到复制构造函数或移动构造函数被调用的信息,因为编译器直接在 myObj 的存储位置构造了 obj。
优点
- 性能提升:避免不必要的复制和移动操作,特别是对于大对象或资源密集型对象。
- 简化语义 :程序员不需要依赖于编译器是否会应用(N)RVO来保证性能,因为现在这些优化是由语言规范保证的。
临时物质化(Temporary Materialication)
在 C++17 中引入了一个重要的概念:临时物质化(Temporary Materialization) 。这个特性涉及到临时对象的创建,尤其是在需要对象实体时,如在传递参数、初始化、返回值等场景中。
背景
在早期的 C++ 标准中,临时对象(比如由表达式产生的未命名对象)的行为有时候可能会造成理解和使用上的混淆,特别是在它们与引用绑定、返回值和函数参数传递等方面。C++17 对这些规则进行了明确,确保临时对象的行为更加直观和可预测。
临时物质化的定义
临时物质化是指在需要一个完整的对象时,将一个临时的prvalue(纯右值)表达式转换为一个临时对象的过程。这主要发生在以下几种情况:
- 当 prvalue 需要作为引用的初始化值时: 如果一个 prvalue 被用作初始化一个引用,那么这个 prvalue 将会物质化为一个临时对象,以便引用可以绑定到它上面。
- 在 prvalue 作为函数参数传递时: 如果函数参数是按值传递的,而传递的实参是 prvalue,那么这个 prvalue 将物质化为一个临时对象,然后将其传递给函数。
- 在 prvalue 作为函数的返回值时 : 当函数返回一个 prvalue 时,这个 prvalue 通常会物质化为一个临时对象,特别是在涉及到返回类型转换时。
示例
下面是一些示例,展示了 C++17 中临时物质化的具体应用:
cpp
#include <iostream>
struct A {
int value;
A(int v) : value(v) { std::cout << "A(" << value << ") constructed\n"; }
A(const A& other) : value(other.value) { std::cout << "A copied\n"; }
};
A getA() {
return A(5); // 返回 prvalue
}
void takeA(A a) {
std::cout << "Received A: " << a.value << std::endl;
}
int main() {
const A& aRef = A(10); // prvalue 物质化为临时对象,引用绑定到它
takeA(A(20)); // prvalue 物质化为临时对象,传递给函数
A myA = getA(); // prvalue 物质化过程
return 0;
}
在这个例子中,每次 A(数字) 被调用时,都创建了一个 prvalue,随后在需要时物质化为一个临时对象。这些临时对象被用来初始化引用、作为函数参数,或直接赋值给变量。
模板新特性
折叠表达式(...)
在了解折叠表达式前先了解C++11引入的形参包,模板形参包 是一个与可变参数模板(Variadic Templates)紧密相关的概念。形参包允许你在函数或模板定义中接受不确定数量的模板参数或函数参数,使得模板和函数可以具有通用性和灵活性,能够处理任意数量和类型的输入参数。
语法
cpp
( 形参包 运算符... ) // 一元右折叠
( ...运算符 形参包 ) // 一元左折叠
( 形参包 运算符...运算符 初值 ) // 二元右折叠
( 初值 运算符...运算符 形参包 ) // 二元左折叠
其实一元和二元的概念是一样的,只是二元折叠要多一个初值参数。
示例说明
- 一元右折叠
cpp
#include <iostream>
#include <string>
template<typename... Args>
std::string concatenateRight(Args... args) {
return (args + ... + std::string("")); // 一元右折叠
}
int main() {
std::cout << "Concatenate Right: " << concatenateRight("Hello", " ", "World", "!")
<< std::endl; // 打印:Concatenate Right: Hello World!
}
- 一元左折叠
cpp
#include <iostream>
#include <string>
template<typename... Args>
std::string concatenateLeft(Args... args) {
return (std::string("") + ... + args); // 一元左折叠
}
int main() {
std::cout << "Concatenate Left: " << concatenateLeft("Hello", " ", "World", "!")
<< std::endl; // 打印:Concatenate Left: Hello World!
}
- 二元右折叠
cpp
#include <iostream>
template<typename... Args>
int sum(int init, Args... args) {
return (args + ... + init); // 二元右折叠
}
int main() {
std::cout << "Sum with initial 10: " << sum(10, 1, 2, 3) << '\n'; // 打印:16
}
- 二元左折叠
cpp
#include <iostream>
template<typename... Args>
int sum(int init, Args... args) {
return (init + ... + args); // 二元左折叠
}
int main() {
std::cout << "Sum with initial 10: " << sum(10, 1, 2, 3) << '\n'; // 打印:16
}
类模板实参推导
在C++17之前,当你使用模板类时,通常需要显式地指定所有的模板参数。例如,使用标准库中的std::pair
或std::vector
时,你需要这样写:
cpp
std::vector<int> v = {1, 2, 3};
std::pair<int, double> p = {1, 3.14};
但是有了类模板实参推导,你可以省略模板参数:
cpp
std::vector v = {1, 2, 3}; // 推导为 std::vector<int>
std::pair p = {1, 3.14}; // 推导为 std::pair<int, double>
auto占位的非类型模板形参
什么是非类型模板参数
非类型模板参数(Non-type template parameters)是C++模板编程中的一种强大的特性,允许在定义模板时使用一个具体的值而不是类型作为模板参数。这种参数可以是一个整数、枚举、指针或引用,甚至某些类的常量表达式,它们用于生成依赖于这些值的模板实例。
示例说明
cpp
template<auto Value>
class Constant
{
};
int main()
{
std::vector<int> vv{1, 2, 3};
// 使用例子
Constant<5> intConst; // 推导为 Constant<int>
Constant<'a'> charConst; // 推导为 Constant<char>
//Constant<3.14> doubleConst; // 推导为 Constant<double>
}
上面示例代码中使用std::vector
时传递的形参必须是一个类型,而不能是一个值;但使用Constant
模板可以传递具体的值,因为它通过auto
占位符来使编译期间自动推导出具体的类型。
上面示例中在使用
double
类型的值进行实例化时会编译失败,是因为在C++20前非类型模板形参不能是double
类型。