aytony

求古寻论,散虑逍遥。

0%

现代C++部分语法糖

nullptr:空指针

C++11引入,规定空指针的值,替换C++98中的NULL关键字。

C++98中,编译器有这样的语句:#define NULL 0,这使得NULL的类型与int相同,造成了重载函数时可能的混淆问题。如下:

1
2
3
4
void func(int a);
void func(int *a);

func(NULL); // 调用的是 func(int) 而不是 func(int *)

为解决此问题,C++11引入了nullptr关键字为空指针赋值,其类型为nullptr_t,与int类型不同。

值得一提的是,编译器定义nullptr_t的方法很有意思。因为此类型以_t结尾,说明非原生关键字,而是由编译器定义出来。

1
2
3
#if __cplusplus >= 201103L
typedef decltype(nullptr) nullptr_t;
#endif

由上可见,编译器定义nullptr_tnullptr的类型,甚是巧妙。

constexpr:常量表达式

C++11引入,与const关键字使用方法类似,用以修饰一个变量或函数的值为常量表达式,即为编译期常量。

C++标准中,关于数组长度有这样的规定:

The elements field within square brackets [], representing the number of elements in the array, must be a constant expression, since arrays are blocks of static memory whose size must be determined at compile time, before the program runs.

中括号中的数组长度必须为常量表达式,因为数组代表着一组在编译时就被确定的静态存储区域,在程序运行前就已经被定死。

From: Arrays - C++ Tutorials

在平常代码中,我们经常将const int的值作为数组长度,但实际上这是违反C++标准的,因为const int只要求其初始化后不得再被改变,而不要求其在编译期就计算出结果。换而言之,它并不是合法的常量表达式。我们平常的用法只是编译器提供的合理化扩展。一个反例如下。

1
2
3
4
int a;
scanf("%d", a);
const int b = a + 1;
printf("%d\n", b);

以上代码中的bconst int类型,但是却在运行时才被初始化,其值也无法在编译期计算出来。为此,我们才引入了constexpr关键字,其标志着编译时绝对能够准确计算的值和函数。

constexpr修饰变量

在定义一般变量时,只需要写上constexpr关键字即可定义一个编译期常量。constexpr 可以与 const 连用。在为constexpr变量赋值时,赋值号右侧的语句必须也是常量表达式。

1
2
3
4
constexpr int a;
constexpr const double PI = 3.14f;
constexpr double RADIUS = 3.0f;
constexpr double AREA = PI * RADIUS * RADIUS;

constexpr修饰函数返回值

constexpr修饰的函数就变成了“两用函数”:

  • 在传入参数均为constexpr类型时其返回值也为constexpr类型。
  • 在传入参数不均为constexpr类型时其返回值不为constexpr类型,其表现与正常函数相同。
1
2
3
4
5
6
7
8
constexpr int fib(const int n)
{
return n == 1 || n == 2 ? 1 : fib(n - 1) + fib(n - 2);
}

int a = 3;
constexpr b = fib(a); // 返回值不含constexpr,语句不合法
constexpr c = fib(3); // 返回值含constexpr,语句合法

C++14之前,constexpr变量无法参与if, switch, while语句运算。以下代码在C++14及之后的版本才合法:

1
2
3
4
5
6
7
constexpr int fib(const int n)
{
if (n == 1 || n == 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}

if - switch:初始化语句

C++11引入,在if, switch语句括号内可以插入初始化语句。定义出来的变量与if, switch生命周期相同。

1
2
3
4
5
6
7
8
9
if (int a = 1; a != 5)
printf("1\n");

switch (int a = 1; a)
{
case 1:
break;
default:;
}

[[fallthrough]], [[likely]][[unlikely]]

C++20引入,用以给编译器打标记。

[[fallthrough]]标记用于不添加break语句的case语段末尾,可以消除警告。

[[likely]][[unlikely]]可以用于while, if, else ifcase等表达式后面,为CPU分支预测提供参考,可以优化程序运行速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum class MID { ZOE, YASUO, AHRI, LUCIAN, VN };

using enum MID;
switch(m)
{
[[unlikely]] case LUCIAN:
[[fallthrough]];

[[unlikely]] case VN:
printf("F U UP ADC!\n");
break;

case ZOE:
printf("Good morning, Aatrox!\n");
[[fallthrough]];

case AHRI:
printf("We are wizards!\n");
break;

[[likely]] default: // YASUO
printf("EEEEEEEEEEEEEEEEEEE\n");
}

range-based for:基于范围的for循环语句

C++11引入,给予for循环新写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
int arr[]{1, 2, 3, 4, 5};

// a为只读
for (int a : arr)
printf("%d ", a); // 输出: 1 2 3 4 5

// a可读写
for (int &a : arr)
printf("%d ", ++a); // 输出: 2 3 4 5 6

// 自动推导a的类型
for (auto a : arr)
printf("%d ", a);

C++20起,range-based for支持初始化语句。

1
2
3
4
for(auto arr = getArr(); auto a : arr)
{
...
}

structured binding:结构化绑定

C++17引入,可以实现一行内拆包的操作。

1
2
3
std::pair<int, char> p{1, 'a'};
auto [i, c] = p;
// i = 1, c = 'a'

对于自己定义的结构体也可以直接拆包,但是无法完全拆包继承类或者类的private项。

1
2
3
4
5
6
7
8
9
10
struct top
{
const char *name;
const char *nickname;
};

top dog{"Nasus", "GouTou"};
auto [name, nickname] = dog;
printf("%s %s\n", name, nickname);
// 输出: Nasus Goutou

与元组结合实现函数的多返回值

std::tupleC++11中引入。我们可以利用结构化绑定和std::tuple快速实现函数的多返回值。使用std::tuple需要包含<tuple>头文件。

1
2
3
4
5
6
7
8
// 指定函数返回含有一个float,一个char和一个int的元组类型
std::tuple<float, char, int> func()
{
return {1.0f, 'a', -3};
}

auto [fval, cval, ival] = func(); // 利用结构化绑定对返回的元组直接拆包
// fval = 1.0f, cval = 'a', ival = -3