第二部分-语言可用性的强化

语言可用性的强化

常量

nullptr

nullptr 出现的目的是为了替代 NULL。在某种意义上来说,传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。

constexpr

C++ 本身已经具备了常量表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。

C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证该函数在编译期就应该是一个常量表达式。

此外,constexpr 修饰的函数可以使用递归。
从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,

变量及其初始化

###if/switch变量声明强化
在传统 C++ 中,变量的声明虽然能够位于任何位置,甚至于 for 语句内能够声明一个临时变量 int,但始终没有办法在 if 和 switch 语句中声明一个临时的变量。
C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成这一操作:

1
2
3
4
5
// 将临时变量放到 if 语句内
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4;
}

初始化列表

在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、 POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体) 类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。 而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行。 这些不同方法都针对各自对象,不能通用。

为解决这个问题,C++11 首先把初始化列表的概念绑定到类型上,称其为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <initializer_list>
#include <vector>
#include <iostream>

class MagicFoo {
public:
std::vector<int> vec;
MagicFoo(std::initializer_list<int> list) {
for (std::initializer_list<int>::iterator it = list.begin();
it != list.end(); ++it)
vec.push_back(*it);
}
};
int main() {
// after C++11
MagicFoo magicFoo = {1, 2, 3, 4, 5};
MagicFoo magicFoo {1, 2, 3, 4, 5};

std::cout << "magicFoo: ";
for (std::vector<int>::iterator it = magicFoo.vec.begin();
it != magicFoo.vec.end(); ++it)
std::cout << *it << std::endl;
}

这种构造函数被叫做初始化列表构造函数,具有这种构造函数的类型将在初始化时被特殊关照。

初始化列表除了用在对象构造上,还能将其作为普通函数的形参,例如:

1
2
3
4
5
6
7
public:
void foo(std::initializer_list<int> list) {
for (std::initializer_list<int>::iterator it = list.begin();
it != list.end(); ++it) vec.push_back(*it);
}

magicFoo.foo({6,7,8,9});

结构化绑定

结构化绑定提供了类似其他语言中提供的多返回值的功能。在容器一章中,我们会学到 C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <tuple>

std::tuple<bool, int,int> GetUserAge(const std::string& user_name)
{
std::tuple<bool, int,int> result;

if (user_name.compare("xiaoli") == 0)
{
result = std::make_tuple(true, 18,0);
}
else
{
result = std::make_tuple(false, -1,-1);
}

return result;
}

int main()
{
std::tuple<bool, int,int> result = GetUserAge("xiaolili");

bool work_status;
int age;
int user_id;

std::tie(work_status, age, user_id) = result;
std::cout << "查询结果:" << work_status << " " << "年龄:" << age <<" "<<"用户id:"<<user_id <<std::endl;
getchar();
return 0;
}

C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <tuple>

std::tuple<int, double, std::string> f() {
return std::make_tuple(1, 2.3, "456");
}

int main() {
auto [x, y, z] = f();
std::cout << x << ", " << y << ", " << z << std::endl;
return 0;
}

类型推导

C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯。

auto

使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。你应该在前面的小节里看到了传统 C++ 中冗长的迭代写法:

1
2
3
4
5
6
7
// 在 C++11 之前
// 由于 cbegin() 将返回 vector<int>::const_iterator
// 所以 it 也应该是 vector<int>::const_iterator 类型
for(vector<int>::const_iterator it = vec.cbegin(); it != vec.cend(); ++it)

//C++11之后
for (auto it = list.begin(); it != list.end(); ++it)

注意:auto 还不能用于推导数组类型:

1
2
3
4
auto auto_arr2[10] = {arr}; // 错误, 无法推导数组元素类型

2.6.auto.cpp:30:19: error: 'auto_arr2' declared as array of 'auto'
auto auto_arr2[10] = {arr};

decltype

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 typeof 很相似:decltype(表达式)

1
2
3
4
5
6
7
8
9
auto x = 1;
auto y = 2;
decltype(x+y) z;
if (std::is_same<decltype(x), int>::value)
std::cout << "type x == int" << std::endl;
if (std::is_same<decltype(x), float>::value)
std::cout << "type x == float" << std::endl;
if (std::is_same<decltype(x), decltype(z)>::value)
std::cout << "type z == type x" << std::endl;

其中,std::is_same<T, U> 用于判断 T 和 U 这两个类型是否相等。输出结果为:

1
2
type x == int
type z == type x

尾返回类型推导

在传统 C++ 中我们必须这么写:

1
2
3
4
template<typename R, typename T, typename U>
R add(T x, U y) {
return x+y;
}

这样的代码其实变得很丑陋,因为程序员在使用这个模板函数的时候,必须明确指出返回类型。但事实上我们并不知道 add() 这个函数会做什么样的操作,以及获得一个什么样的返回类型。

1
decltype(x+y) add(T x, U y)

但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时,x 和 y 尚未被定义。为了解决这个问题,C++11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:

1
2
3
4
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}

从 C++14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:

1
2
3
4
template<typename T, typename U>
auto add3(T x, U y){
return x + y;
}

decltype(auto)

decltype(auto) 是 C++14 开始提供的一个略微复杂的用法。
简单来说,decltype(auto) 主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的指定 decltype 的参数表达式。考虑看下面的例子,当我们需要对下面两个函数进行封装时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::string  lookup1();
std::string& lookup2();
//封装实现
std::string look_up_a_string_1() {
return lookup1();
}
std::string& look_up_a_string_2() {
return lookup2();
}
//C++14以后
decltype(auto) look_up_a_string_1() {
return lookup1();
}
decltype(auto) look_up_a_string_2() {
return lookup2();
}

控制流

if constexpr

我们知道了 C++11 引入了 constexpr 关键字,它将表达式或函数编译为常量结果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高?C++17 将 constexpr 这个关键字引入到 if 语句中,允许在代码中声明常量表达式的判断条件,考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

template<typename T>
auto print_type_info(const T& t) {
if constexpr (std::is_integral<T>::value) {
return t + 1;
} else {
return t + 0.001;
}
}
int main() {
std::cout << print_type_info(5) << std::endl;
std::cout << print_type_info(3.14) << std::endl;
}

在编译时,实际代码就会表现为如下:

1
2
3
4
5
6
7
8
9
10
int print_type_info(const int& t) {
return t + 1;
}
double print_type_info(const double& t) {
return t + 0.001;
}
int main() {
std::cout << print_type_info(5) << std::endl;
std::cout << print_type_info(3.14) << std::endl;
}

区间for迭代

终于,C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句,我们可以进一步简化前面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> vec = {1, 2, 3, 4};
if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) *itr = 4;
for (auto element : vec)
std::cout << element << std::endl; // read only
for (auto &element : vec) {
element += 1; // writeable
}
for (auto element : vec)
std::cout << element << std::endl; // read only
}