导论
c17新特性引入了许多新的语法,这些语法特性更加清晰,不像传统语法,语义飘忽不定,比如‘a’你根本不知道是宽字符还是UTF-8 字符。以及测试i++ ++i,最后结果到底是多少。这种问题很大情况是根据编译器的优化进行猜测,不同环境,得出不同结果。而c17对这些问题给出完美的解决方案!
字符字面量
理论
官方给出的语法:
-
普通字符字面量:
'c-char'
-
UTF-8 字符字面量:
u8'c-char'
-
UTF-16 字符字面量:
u'c-char'
-
UTF-32 字符字面量:
U'c-char'
-
宽字符字面量:
L'c-char'
-
普通多字符字面量:
'c-char-sequence'
-
宽字符多字符字面量:
L'c-char-sequence'
不太懂??基础薄弱!!
-
普通字符字面量:
- 形式:
'c-char'
- 表示: 一个单个的普通字符字面量,通常为 8 位 ASCII 字符。
- 类型:
char
- 形式:
-
UTF-8 字符字面量:
- 形式:
u8'c-char'
- 表示: 一个单个的 UTF-8 编码的字符字面量。
- 类型:
char8_t
(C++20 引入)
- 形式:
-
UTF-16 字符字面量:
- 形式:
u'c-char'
- 表示: 一个单个的 UTF-16 编码的字符字面量。
- 类型:
char16_t
- 形式:
-
UTF-32 字符字面量:
- 形式:
U'c-char'
- 表示: 一个单个的 UTF-32 编码的字符字面量。
- 类型:
char32_t
- 形式:
-
宽字符字面量:
- 形式:
L'c-char'
- 表示: 一个单个的宽字符字面量。宽字符的大小由实现定义,通常为 16 位或 32 位。
- 类型:
wchar_t
- 形式:
-
普通多字符字面量:
- 形式:
'c-char-sequence'
- 表示: 一个字符序列,由多个普通字符组成。
- 类型: 数组类型
char[N]
,其中N
为字符序列的长度。
- 形式:
-
宽字符多字符字面量:
- 形式:
L'c-char-sequence'
- 表示: 一个字符序列,由多个宽字符组成。
- 类型: 数组类型
wchar_t[N]
,其中N
为字符序列的长度。
- 形式:
这些不同类型的字符字面量主要有以下区别:
-
字符编码:
- 普通字符为 8 位 ASCII 编码。
- UTF-8、UTF-16 和 UTF-32 字符分别采用 UTF-8、UTF-16 和 UTF-32 编码。
- 宽字符的编码由实现定义,通常为 16 位或 32 位。
-
表示范围:
- UTF-8、UTF-16 和 UTF-32 字符可以表示更广泛的字符集,包括非 ASCII 字符。
- 宽字符的表示范围由实现定义。
-
内存占用:
- 普通字符和 UTF-8 字符占用 1 个字节。
- UTF-16 字符占用 2 个字节。
- UTF-32 字符和宽字符占用 4 个字节。
选择合适的字符类型取决于您的具体需求。对于仅需要处理 ASCII 字符的场景,使用普通字符就足够了。如果需要支持更广泛的字符集,可以考虑使用 UTF-8、UTF-16 或 UTF-32 字符。如果需要与遗留代码兼容,或者需要处理宽字符的场景,则可以使用宽字符。 现在我们可以定义出适合的字符串类型,写出最佳的程序。
实践
普通字符字面量:
char c1 = 'a';
char c2 = '\n';
char c3 = '\x2a'; // 等同于 '*'
UTF-8 字符字面量:
char8_t c1 = u8'a';
// char8_t c2 = u8'¢'; // 错误,¢无法用单个UTF-8码元表示
// char8_t c3 = u8'猫'; // 错误,猫无法用单个UTF-8码元表示
// char8_t c4 = u8'🍌'; // 错误,🍌无法用单个UTF-8码元表示
UTF-16 字符字面量:
char16_t c1 = u'a';
char16_t c2 = u'¢';
char16_t c3 = u'猫';
// char16_t c4 = u'🍌'; // 错误,🍌无法用单个UTF-16码元表示
UTF-32 字符字面量:
char32_t c1 = U'a';
char32_t c2 = U'¢';
char32_t c3 = U'猫';
char32_t c4 = U'🍌';
宽字符字面量:
wchar_t wc1 = L'a';
wchar_t wc2 = L'¢';
wchar_t wc3 = L'猫';
wchar_t wc4 = L'🍌'; // 在Windows上,这可能是非法的
普通多字符字面量:
int mc1 = 'ab'; // 实现定义的值
int mc2 = 'abc'; // 实现定义的值
宽字符多字符字面量:
wchar_t wmc1 = L'ab'; // 实现定义的值
有关变量的新语法
变量的评估顺序
表达式的求值顺序:
-
C++ 标准没有规定表达式中各个子表达式的求值顺序,除了一些特定的情况。
-
通常编译器可以自由选择求值顺序,只要最终结果与按照左到右的顺序求值一致。
x = (++x, ++y); 传统cpp并没有标准的定义到底谁先执行,都是由编译器优化,这就会造成环境不同,效果不同,争的你死我活。现在c17是这样解决的。
C++ 标准库中的 std::evaluate
和 std::as_const
这两个工具函数:
-
std::evaluate
(C++23):-
功能: 强制求值一个表达式,确保其副作用按照预期顺序发生。
-
声明:
template<class T> constexpr T&& evaluate(T&& t) noexcept;
-
使用:
int x = 0, y = 0; x = (++x, ++y); // 存在顺序未定义,可能 x 或 y 先增加 x = std::evaluate((++x, ++y)); // 确保 x 和 y 按从左到右的顺序增加
-
作用: 帮助开发者明确表达式的求值顺序,避免由于未定义行为导致的bug。
-
-
std::as_const
(C++17):-
功能: 获取一个表达式的常量引用,避免意外修改。
-
声明:
template<class T> constexpr const T& as_const(T& t) noexcept;
-
使用:
std::string s = "hello"; std::string_view sv = s; // 可能意外修改 s std::string_view sv = std::as_const(s); // 确保 sv 只能读取 s 的内容
-
作用: 帮助开发者强制使用只读引用,防止无意中修改原对象。
-
总之, std::evaluate
和 std::as_const
是 C++ 标准库提供的两个有用的工具函数,前者解决表达式求值顺序的问题,后者解决引用可能被意外修改的问题。开发者可以在需要时使用它们来编写更加健壮的代码。
ifswitch初始化
这种多说无益,直接看示例就明白了。
auto lambda = [](int x) {
if (int y = x * x; y > 100) {
return y;
} else {
return 0;
}
};
int result = lambda(11); // result 为 121
auto lambda = [](char c) {
switch (int x = c; x) {
case 'a':
return 1;
case 'b':
return 2;
default:
return 0;
}
};
int result = lambda('b'); // result 为 2
结构化绑定声明 简化代码。
可以看我往期文章,在此简单举个例子
#include <iostream>
#include <tuple>
int main() {
std::tuple<int, char, std::string> t{42, 'a', "hello"};
//变量的数量必须与复合类型的元素数量一致。
auto [x, y, z] = t;
std::cout << x << ", " << y << ", " << z << '\n'; // 输出: 42, a, hello
return 0;
}
左值引用和右值引用
可以看我往期文章,就不再赘述,这是重中之重。
constexpr consteval 编译时求值
constexpr
和 consteval
是 C++11 和 C++20 引入的两个关键字,它们都用于在编译时执行计算,但是它们之间有一些区别:
-
constexpr:
constexpr
表示该变量或函数在编译时就可以计算出它的值。constexpr
函数可以在编译时执行,也可以在运行时执行。- 如果
constexpr
函数在编译时无法计算出结果,编译器会尝试在运行时执行该函数。 constexpr
可以用于变量、函数和类的成员函数。
-
consteval:
consteval
是 C++20 引入的,它比constexpr
更加严格。consteval
函数必须在编译时就能计算出结果,不能在运行时执行。- 如果
consteval
函数在编译时无法计算出结果,编译器会报错。 consteval
只能用于函数,不能用于变量或类的成员函数。
举例
constexpr 变量
constexpr int x = 42;
constexpr std::array<int, 3> arr = {1, 2, 3};
constexpr 构造函数
struct Point {
constexpr Point(int x, int y) : x(x), y(y) {}
int x, y;
};
constexpr Point p(1, 2); // 在编译时创建 p 对象
consteval关键字
consteval int square(int x) {
return x * x;
}
constexpr int y = square(3); // 在编译时计算出 y = 9
总的来说:
constexpr
是一种"可能在编译时计算"的函数或变量,而consteval
是一种"必须在编译时计算"的函数。constexpr
提供了更大的灵活性,但consteval
提供了更严格的编译时计算保证。- 开发者应该根据具体需求选择使用
constexpr
还是consteval
。如果一个函数在编译时就能计算出结果,使用consteval
更合适;如果需要在运行时也能正常执行,使用constexpr
更合适。
任意类型变量 any variant
variant 编译期
std::variant
是 C++17 引入的一个非常有用的类型,它可以表示一个在多个类型之间进行选择的值。它提供了一种安全和高效的方式来处理可能出现的多种类型。
以下是 std::variant
的一些主要特点:
-
可以包含多种类型:
std::variant
可以存储不同类型的值,这些类型由开发者在定义std::variant
时指定。 -
类型安全: 与使用
void*
或者联合(union)相比,std::variant
提供了更好的类型安全性。它可以在编译时就检查访问是否合法,从而避免运行时错误。 -
访问安全:
std::variant
提供了多种安全的访问方式,如std::get
、std::visit
等,可以确保在访问时不会出现未定义行为。 -
无需手动内存管理:
std::variant
会自动管理其包含的值的生命周期,开发者不需要手动分配或释放内存。
any 运行期
std::any
是 C++17 引入的一个很有用的类型,它可以用来存储和传递任意类型的值。它提供了一个安全和高效的方式来处理动态类型的数据。
以下是 std::any
的一些主要特点:
-
可以存储任意类型的值:
std::any
可以存储任何类型的值,包括基本数据类型、自定义类型、数组、指针等。 -
类型安全: 与使用
void*
相比,std::any
提供了更好的类型安全性。它会在运行时检查访问是否合法,从而避免未定义的行为。 -
无需手动内存管理:
std::any
会自动管理其包含的值的生命周期,开发者不需要手动分配或释放内存。 -
支持赋值和拷贝:
std::any
支持赋值和拷贝操作,这使得它可以很方便地在代码中传递和存储值。
示例
#include <iostream>
#include <any>
#include <variant>
#include <string>
int main() {
// 使用 std::any
std::any anyValue = 42;
std::cout << "std::any value: " << std::any_cast<int>(anyValue) << std::endl;
anyValue = std::string("hello");
std::cout << "std::any value: " << std::any_cast<std::string>(anyValue) << std::endl;
// 使用 std::variant
std::variant<int, std::string> variantValue = 42;
std::cout << "std::variant value: " << std::get<int>(variantValue) << std::endl;
variantValue = std::string("hello");
std::cout << "std::variant value: " << std::get<std::string>(variantValue) << std::endl;
return 0;
}
any
和 variant区别
std::any
和 std::variant
都是 C++17 引入的非常有用的类型,但它们在功能和使用场景上有一些区别:
-
存储类型:
-
std::any
可以存储任意类型的值,包括自定义类型、数组、指针等。 -
std::variant
只能存储在定义时指定的有限个类型中的一种。
-
-
类型安全:
-
std::any
提供了运行时类型检查,通过std::any_cast
访问时会检查类型是否匹配,不匹配则抛出std::bad_any_cast
异常。 -
std::variant
则是在编译时就确定了可能存储的类型,通过std::get
等函数访问时也可以在编译时检查类型是否匹配。
-
-
访问方式:
-
std::any
通过std::any_cast
进行访问,需要显式指定类型。 -
std::variant
可以使用std::get
、std::visit
等多种方式进行访问。
-
-
使用场景:
-
std::any
适用于需要处理动态类型数据的场景,如插件系统、配置文件解析等。 -
std::variant
则更适用于在有限的几种类型之间进行选择的场景,如函数重载、状态机等。
-
隐藏转换
这个模块可谓是本文的重点,坑点是最多的。
临时对象具体化 抛砖
在 C++ 中,临时对象的具体化是一个非常重要的概念。临时对象是在表达式求值过程中创建的短暂对象,它们通常会在表达式结束后被销毁。
std::string getStr() {
return "hello";
}
std::string s = getStr(); // 临时对象被具体化并赋值给s
在这个例子中, getStr() 函数返回一个临时的 std::string 对象,该对象会在 main()
函数中被具体化并赋值给 s。
int x = 1, y = 2;
int z = x + y; // 临时对象被具体化并赋值给z
在这个例子中,x + y 表达式创建了一个临时的 int 对象,该对象会被具体化并赋值给 z。
std::function<int(int)> get_lambda() {
return [](int x) { return x * x; };
}
auto square_lambda = get_lambda();
int result = square_lambda(5); // 结果是 25
在这个例子中:
1. `get_lambda()` 函数返回一个 lambda 表达式。
2. 这个 lambda 表达式产生了一个临时的 lambda 对象。
3. 这个临时的 lambda 对象被用于初始化 `square_lambda` 变量。
这个临时的 lambda 对象就是通过"临时对象具体化"的过程产生的。编译器会确保这个临时对象的生命周期足够长,以满足初始化 `square_lambda` 的需求。
保证拷贝省略 引玉
保证拷贝省略(Guaranteed Copy Elision, GCE)并不是 C++17 引入的新特性,它实际上是在 C++11 中引入的。
C++11中引入了以下几种情况下的拷贝省略:
-
返回值优化(RVO):
- 当函数返回一个局部对象时,编译器可以直接在调用方的位置构造该对象,而无需进行拷贝。
-
移动语义优化:
- 当返回一个临时对象时,编译器可以使用移动构造函数而不是拷贝构造函数。
-
构造函数参数优化:
- 当构造函数的参数是一个临时对象时,编译器可以直接在目标位置构造该对象,而无需进行拷贝。
C++17进一步扩展了拷贝省略的范围,增加了以下几种情况:
-
无参数构造函数拷贝省略:
- 当函数返回一个局部对象,且该对象没有参数的构造函数时,编译器可以直接在调用方的位置构造该对象,而无需进行拷贝。
-
聚合类型拷贝省略:
- 当函数返回一个聚合类型(如数组或结构体)的局部对象时,编译器可以直接在调用方的位置构造该对象,而无需进行拷贝。
总的来说,C++11和C++17中的拷贝省略优化可以显著提高程序的性能,减少不必要的拷贝操作。编译器会自动进行这些优化,开发者无需手动干预。
性能优化巅峰
先举个例子
#include <iostream>
class Noisy {
public:
Noisy() {
std::cout << "Noisy object constructed at " << this << '\n';
}
Noisy(const Noisy& other) {
std::cout << "Noisy object copy-constructed at " << this << '\n';
}
Noisy(Noisy&& other) noexcept {
std::cout << "Noisy object move-constructed at " << this << '\n';
}
Noisy& operator=(const Noisy& other) {
if (this != &other) {
std::cout << "Noisy object copy-assigned at " << this << '\n';
}
return *this;
}
Noisy& operator=(Noisy&& other) noexcept {
if (this != &other) {
std::cout << "Noisy object move-assigned at " << this << '\n';
}
return *this;
}
~Noisy() {
std::cout << "Noisy object destructed at " << this << '\n';
}
};
Noisy f(){
// c11传统调用 创建一个临时 Noisy 对象,然后将其拷贝构造或移动构造到 v 中
// c17 编译器可以直接在 v 上构造 Noisy 对象,省略掉不必要的拷贝/移动操作。 称为(since C++17) "保证拷贝省略"
Noisy v = Noisy(); //注意这里没有任何函数调用!!! 它是怎么做到给返回值给变量v赋值的??
/*
解答:
1.编译器识别到 Noisy v = Noisy(); 是一个直接初始化语句。
2.它会在 v 的位置直接构造一个 Noisy 对象(Noisy v),而不是先创建一个临时对象,然后再拷贝或移动到 v 中。
3.这个构造过程会调用 Noisy 类的默认构造函数,输出 "Noisy object constructed at 0x[地址]"。
4.最终,v 就直接成为一个 Noisy 类型的对象,不需要经过任何拷贝或移动操作。
*/
return v;
}
void g(Noisy arg)
{
std::cout << "&arg = " << &arg << '\n';
}
int main()
{
// c11 会执行一次拷贝或移动操作,将 v 的内容复制或移动到返回值中。
//C++17引入命名返回值优化( NRVO)允许编译器直接在返回值位置构造对象(不调用构造函数),避免不必要的拷贝或移动。
Noisy v = f(); //同理 f()返回值v 如何实现给v初始化的?
/*
如果 f() 返回一个临时 Noisy 对象或者返回一个命名的 Noisy 对象(例如一个局部变量),编译器会尝试应用 RVO 和 NRVO 等优化,
尽量避免不必要的拷贝和移动操作。
编译器可以直接在 v 的位置构造这个返回的 Noisy 对象,避免任何拷贝或移动。
*/
std::cout << "&v = " << &v << '\n';
g(f());// (since C++17) "拷贝省略"
}
输出
constructed at 0x7fff1d765096 解释:代码行 Noisy v = Noisy(); 的调用
&v = 0x7fff1d765096
constructed at 0x7fff1d765097 解释:代码行 Noisy v = Noisy(); 的调用
&arg = 0x7fff1d765097
destructed at 0x7fff1d765097
destructed at 0x7fff1d765096
C++11 和 C++17 在这些优化上的差异:
-
临时变量的初始化:
- C++11 及更早的版本中,
Noisy v = Noisy();
会先创建一个临时Noisy
对象,然后将其拷贝或移动到v
中。这会涉及一次构造和一次拷贝/移动操作。 - C++17 引入了"保证拷贝省略"(Guaranteed Copy Elision, GCE)特性,编译器可以直接在
v
上构造Noisy
对象,省略掉不必要的拷贝/移动操作。
- C++11 及更早的版本中,
-
函数返回值的优化:
- C++11 及更早的版本中,
return v;
会执行一次拷贝或移动操作,将v
的内容复制或移动到返回值中。 - C++17 引入的"命名返回值优化"(Named Return Value Optimization, NRVO)允许编译器直接在返回值位置构造对象,避免不必要的拷贝或移动。
- C++11 及更早的版本中,
-
函数参数的传递:
- C++11 及更早的版本中,
g(f());
会先创建一个临时Noisy
对象,然后将其拷贝或移动到arg
中。 - C++17 的"拷贝省略"(Copy Elision)特性允许编译器直接在
arg
上构造Noisy
对象,避免不必要的拷贝或移动。
- C++11 及更早的版本中,
上列的例子,应该能让读者更清楚的对c17”保证拷贝省略“进一步的理解,最后我们在整理一下思路,
临时变量的初始化: Noisy v = Noisy() -> Nosiy v();
函数返回值的优化: Noisy v = {Noisy v; return v;} -> Nosiy v();
函数参数的传递:
主函数 nrvo优化:
Noisy v = f(); ->
Noisy v = {Noisy v = Noisy(); return v; }-> //临时变量初始化的优化
Noisy v = {Noisy v(); return v; } ->
Noisy v = {return v} -> //函数返回值的优化
Noisy v = Noisy() ->
Noisy v;
g(f())
g(Noisy arg = {Noisy v = Noisy(); return v;})
g(Noisy arg = {Noisy v(); return v;})
g(Noisy arg )
隐式转换
C++ 隐式转换的内容总结如下:
-
隐式转换的顺序:
-
标准转换序列 - 包括值转换和一些数值转换
-
用户定义转换 - 通过单参数构造函数或转换函数进行
-
可能的额外标准转换序列
-
-
值转换:
-
左值到右值
-
数组到指针
-
函数到指针
-
临时对象化 - 将右值转换为左值
-
-
整数提升:
-
将小整数类型提升为 int 或 unsigned int
-
-
浮点提升:
-
float 转换为 double
-
-
数值转换:
-
整数间转换
-
浮点间转换
-
浮点和整数间转换
-
指针间转换
-
指针到成员间转换
-
布尔转换
-
-
限定符转换:
-
在相似类型间添加或删除 const/volatile 限定符
-
-
上下文转换:
-
在特定上下文中进行的隐式转换,如条件表达式、逻辑运算符等
-
举例
标准转换序列:
int x = 3.14; // float 到 int 的隐式转换
用户定义转换:
class Rational {
public:
Rational(int numerator, int denominator = 1) {
// 用户定义的单参数构造函数
}
};
Rational r = 5; // int 到 Rational 的隐式转换 5->Rational 因为没加explicit
值转换:
int* p = new int[10];
int x = p[0]; // 数组到指针的隐式转换,然后左值到右值的隐式转换
整数提升:
char c = 'a';int i = c; // char 提升到 int
浮点提升:
float f = 3.14f;
double d = f; // float 提升到 double
限定符转换:
const int* p = new int;
int* q = const_cast<int*>(p); // 从 const int* 到 int* 的转换
上下文转换:
if (Rational r = 5) { // Rational 到 bool 的隐式转换
// ...
}
总结
总的来说,这篇文章全面介绍了C++17中的一些重要语法和概念变化,对于了解和掌握C++17的新特性很有帮助。文章内容丰富,逻辑清晰,可以作为C++17学习的良好参考资料。