语言可用性的强化

常量:constexpr

constexpr

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

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
#include <iostream>
#define LEN 10
int len_foo() {
int i = 2;
return i;
}
constexpr int len_foo_constexpr() {
return 5;
}
constexpr int fibonacci(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}
int main() {
char arr_1[10]; // 合法
char arr_2[LEN]; // 合法
int len = 10;
// char arr_3[len]; // 非法
const int len_2 = len + 1;
constexpr int len_2_constexpr = 1 + 2 + 3;
// char arr_4[len_2]; // 非法
char arr_4[len_2_constexpr]; // 合法
// char arr_5[len_foo()+5]; // 非法
char arr_6[len_foo_constexpr() + 1]; // 合法
std::cout << fibonacci(10) << std::endl;
// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
std::cout << fibonacci(10) << std::endl;
return 0;
}

上面的例子中,char arr_4[len_2] 可能比较令人困惑,因为 len_2 已经被定义为了常量。为什么 char arr_4[len_2] 仍然是非法的呢?这是因为 C++ 标准中数组的长度必须是一个常量表达式,而对于 len_2 而言,这是一个 const 常数而不是一个常量表达式,因此(即便这种行为在大部分编译器中都支持,但是)它是一个非法的行为,我们需要使用接下来即将介绍的 C++11 引入的 constexpr 特性来解决这个问题;而对于 arr_5 来说,C++98 之前的编译器无法得知 len_foo() 在运行期实际上是返回一个常数,这也就导致了非法的产生。

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

if/switch 变量声明强化

在传统 C++ 中,变量的声明虽然能够位于任何位置,甚至于 for 语句内能够声明一个临时变量 int,但始终没有办法在 ifswitch 语句中声明一个临时的变量。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4};
// 在 c++17 之前
const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 2);
if (itr != vec.end()) {
*itr = 3;
}
// 需要重新定义一个新的变量
const std::vector<int>::iterator itr2 = std::find(vec.begin(), vec.end(), 3);
if (itr2 != vec.end()) {
*itr2 = 4;
}
// 将输出 1, 4, 3, 4
for (std::vector<int>::iterator element = vec.begin(); element != vec.end(); ++element)
std::cout << *element << std::endl;
}

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++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。

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

1
2
3
4
5
6
7
8
9
10
#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;
}

类型推导auto和decltype

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

auto

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

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

而有了 auto 之后可以:

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) {
// 从 C++11 起, 使用 auto 关键字进行类型推导
for (auto it = list.begin(); it != list.end(); ++it) {
vec.push_back(*it);
}
}
};
int main() {
MagicFoo magicFoo = {1, 2, 3, 4, 5};
std::cout << "magicFoo: ";
for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {
std::cout << *it << ", ";
}
std::cout << std::endl;
return 0;
}

注意auto 不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们应该使用模板):

1
2
3
4
int add(auto x, auto y);
2.6.auto.cpp:16:9: error: 'auto' not allowed in function prototype
int add(auto x, auto y) {
^~~~

此外,auto 还不能用于推导数组类型:

1
2
3
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 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和sizeof很相似

有时候,我们可能需要计算某个表达式的类型,例如:

1
2
3
auto x = 1;
auto y = 2;
decltype(x+y) z;

decltype 用于推断类型的用法,下面这个例子就是判断上面的变量 x, y, z 是否是同一类型:

1
2
3
4
5
6
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> 用于判断 TU 这两个类型是否相等

尾返回类型推导

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

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

C++11 中这个问题得到解决。虽然你可能马上会反应出来使用 decltype 推导 x+y 的类型,写出这样的代码:

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

C++14中可以这样写

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

decltype(auto)(C++14)

decltype(auto) 是 C++14 开始提供的一个略微复杂的用法。

简单来说,decltype(auto) 主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的指定 decltype 的参数表达式。考虑看下面的例子.

1
2
std::string  lookup1();
std::string& lookup2();

在 C++11 中,封装实现是如下形式:

1
2
3
4
5
6
std::string look_up_a_string_1() { 
return lookup1();
}
std::string& look_up_a_string_2() {
return lookup2();
}

而有了 decltype(auto),我们可以让编译器完成这一件烦人的参数转发:

1
2
3
4
5
6
decltype(auto) look_up_a_string_1() { 
return lookup1();
}
decltype(auto) look_up_a_string_2() {
return lookup2();
}

控制流 if constexpr(C++17)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

模版

尖括号 “>”

在传统 C++ 的编译器中,>>一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:

1
std::vector<std::vector<int>> matrix;

这在传统C++编译器下是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译

类型别名模板

在了解类型别名模板之前,需要理解『模板』『类型』之间的不同。仔细体会这句话:模板是用来产生类型的。在传统 C++ 中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。例如:

1
2
3
4
5
6
7
8
9
template<typename T, typename U>
class MagicType {
public:
T dark;
U magic;
};
// 不合法
template<typename T>
typedef MagicType<std::vector<T>, std::string> FakeDarkMagic;

C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:

通常我们使用 typedef 定义别名的语法是:typedef 原名称 新名称;,但是对函数指针等别名的定义语法却不相同,这通常给直接阅读造成了一定程度的困难。

1
2
3
4
5
6
7
typedef int (*process)(void *);
using NewProcess = int(*)(void *);
template<typename T>
using TrueDarkMagic = MagicType<std::vector<T>, std::string>;
int main() {
TrueDarkMagic<bool> you;
}

变长参数模板

模板一直是 C++ 所独有的黑魔法(一起念:Dark Magic)之一。 在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子, 接受一组固定数量的模板参数;而 C++11 加入了新的表示方法, 允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定

1
template<typename... Ts> class Magic;

模板类 Magic 的对象,能够接受不受限制个数的 typename 作为模板的形式参数,例如下面的定义:

1
2
3
4
class Magic<int,
std::vector<int>,
std::map<std::string,
std::vector<int>>> darkMagic;

既然是任意形式,所以个数为 0 的模板参数也是可以的:class Magic<> nothing;

如果不希望产生的模板参数个数为0,可以手动的定义至少一个模板参数:

1
复制代码template<typename Require, typename... Args> class Magic;

变长参数模板也能被直接调整到到模板函数上。传统 C 中的 printf 函数, 虽然也能达成不定个数的形参的调用,但其并非类别安全。 而 C++11 除了能定义类别安全的变长参数函数外, 还可以使类似 printf 的函数能自然地处理非自带类别的对象。 除了在模板参数中能使用 ... 表示不定长模板参数外, 函数参数也使用同样的表示法代表不定长参数, 这也就为我们简单编写变长参数函数提供了便捷的手段,例如:

1
template<typename... Args> void printf(const std::string &str, Args... args);

那么我们定义了变长的模板参数,如何对参数进行解包呢?

首先,我们可以使用 sizeof... 来计算参数的个数,:

1
2
3
4
template<typename... Ts>
void magic(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}

我们可以传递任意个参数给 magic 函数:

1
2
3
magic(); // 输出0
magic(1); // 输出1
magic(1, ""); // 输出2

其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理手法:

1. 递归模板函数

递归是非常容易想到的一种手段,也是最经典的处理方法。这种方法不断递归地向函数传递模板参数,进而达到递归遍历所有模板参数的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0;
}

2. 变参模板展开(C++17)

你应该感受到了这很繁琐,在 C++17 中增加了变参模板展开的支持,于是你可以在一个函数中完成 printf 的编写:

1
2
3
4
5
template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0) printf2(t...);
}

事实上,有时候我们虽然使用了变参模板,却不一定需要对参数做逐个遍历,我们可以利用 std::bind 及完美转发等特性实现对函数和参数的绑定,从而达到成功调用的目的。

3. 初始化列表展开

递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。

这里介绍一种使用初始化列表展开的黑魔法:

1
2
3
4
5
6
7
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}

在这个代码中,额外使用了 C++11 中提供的初始化列表以及 Lambda 表达式的特性。

通过初始化列表,(lambda 表达式, value)... 将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。 为了避免编译器警告,我们可以将 std::initializer_list 显式的转为 void

面向对象

委托构造

C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = value;
}
};
int main() {
Base b(2);
std::cout << b.value1 << std::endl;
std::cout << b.value2 << std::endl;
}

继承构造

在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利用关键字 using 引入了继承构造函数的概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = value;
}
};
class Subclass : public Base {
public:
using Base::Base; // 继承构造
};
int main() {
Subclass s(3);
std::cout << s.value1 << std::endl;
std::cout << s.value2 << std::endl;
}