2.1 常量
nullptr
nullptr
出现的目的是为了替代 NULL
。在某种意义上来说,传统 C++ 会把 NULL
、0
视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0)
,有些则会直接将其定义为 0
。
C++ 不允许直接将 void *
隐式转换到其他类型。但如果编译器尝试把 NULL
定义为 ((void*)0)
,那么在下面这句代码中:
char *ch = NULL; |
---|
没有了 void *
隐式转换的 C++ 只好将NULL
定义为 0
。而这依然会产生新的问题,将 NULL
定义成 0 将导致 C++
中重载特性发生混乱。考虑下面这两个 foo
函数:
void foo(char*);void foo(int); |
---|
那么 foo(NULL);
这个语句将会去调用 foo(int)
,从而导致代码违反直觉。
为了解决这个问题,C++11 引入了 nullptr
关键字,专门用来区分空指针、0。而 nullptr
的类型为 nullptr_t
,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
constexpr
C++ 本身已经具备了常量表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。
C++11 提供了 constexpr
让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 len_foo
在编译期就应该是一个常量表达式。
此外,constexpr
的函数可以使用递归:
constexpr int fibonacci(const int n) { return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);} |
---|
从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 C++11 的标准下是不能够通过编译的:
constexpr int fibonacci(const int n) { if(n == 1) return 1; if(n == 2) return 1; return fibonacci(n-1) + fibonacci(n-2);} |
---|
为此,我们可以写出下面这类简化的版本来使得函数从 C++11 开始即可用:
constexpr int fibonacci(const int n) { return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);} |
---|
2.2 变量及其初始化
c++17 允许if/switch中声明临时变量
初始化列表
初始化是一个非常重要的语言特性,最常见的就是在对象进行初始化时进行使用。 在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、 POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体) 类型都可以使用 {}
进行初始化,也就是我们所说的初始化列表。 而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 ()
进行。 这些不同方法都针对各自对象,不能通用。例如:
#include |
---|
为了解决这个问题,C++11 首先把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list
,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如:
#include |
---|
这种构造函数被叫做初始化列表构造函数,具有这种构造函数的类型将在初始化时被特殊关照。
初始化列表除了用在对象构造上,还能将其作为普通函数的形参,例如:
public: void foo(std::initializer_list |
---|
其次,C++11 还提供了统一的语法来初始化任意的对象,例如:
Foo foo2 {3, 4}; |
---|
结构化绑定
结构化绑定提供了类似其他语言中提供的多返回值的功能。在容器一章中,我们会学到 C++11 新增了 std::tuple
容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie
对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。
C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码:
#include |
---|
关于 auto
类型推导会在 auto 类型推导一节中进行介绍。
2.3 类型推导
auto
auto
在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register
并存。在传统 C++ 中,如果一个变量没有声明为 register
变量,将自动被视为一个 auto
变量。而随着 register
被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 auto
的语义变更也就非常自然了。
使用 auto
进行类型推导的一个最为常见而且显著的例子就是迭代器。
注意:auto
不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们应该使用模板):
int add(auto x, auto y);2.6.auto.cpp:16:9: error: ‘auto’ not allowed in function prototypeint add(auto x, auto y) { ^~~~ |
---|
此外,auto
还不能用于推导数组类型:
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(表达式) |
---|
有时候,我们可能需要计算某个表达式的类型,例如:
auto x = 1;auto y = 2;decltype(x+y) z; |
---|
你已经在前面的例子中看到 decltype
用于推断类型的用法,下面这个例子就是判断上面的变量 x, y, z
是否是同一类型:
if (std::is_same |
---|
其中,std::is_same<T, U>
用于判断 T
和 U
这两个类型是否相等。输出结果为:
type x == inttype z == type x |
---|
尾返回类型推导
你可能会思考,在介绍 auto
时,我们已经提过 auto
不能用于函数形参进行类型推导,那么 auto
能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写:
template |
---|
注意:typename 和 class 在模板参数列表中没有区别,在 typename 这个关键字出现之前,都是使用 class 来定义模板参数的。但在模板中定义有嵌套依赖类型的变量时,需要用 typename 消除歧义
这样的代码其实变得很丑陋,因为程序员在使用这个模板函数的时候,必须明确指出返回类型。但事实上我们并不知道 add()
这个函数会做什么样的操作,获得一个什么样的返回类型。
在 C++11 中这个问题得到解决。虽然你可能马上会反应出来使用 decltype
推导 x+y
的类型,写出这样的代码:
decltype(x+y) add(T x, U y) |
---|
但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时,x
和 y
尚未被定义。为了解决这个问题,C++11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:
template |
---|
令人欣慰的是从 C++14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:
template |
---|
可以检查一下类型推导是否正确:
// after c++11auto w = add2 |
---|
decltype(auto)
decltype(auto)
是 C++14 开始提供的一个略微复杂的用法。
要理解它你需要知道 C++ 中参数转发的概念,我们会在语言运行时强化一章中详细介绍,你可以到时再回来看这一小节的内容。
简单来说,decltype(auto)
主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的指定 decltype
的参数表达式。考虑看下面的例子,当我们需要对下面两个函数进行封装时:
std::string lookup1();std::string& lookup2(); |
---|
在 C++11 中,封装实现是如下形式:
std::string look_up_a_string_1() { return lookup1();}std::string& look_up_a_string_2() { return lookup2();} |
---|
而有了 decltype(auto)
,我们可以让编译器完成这一件烦人的参数转发:
decltype(auto) look_up_a_string_1() { return lookup1();}decltype(auto) look_up_a_string_2() { return lookup2();} |
---|
2.4 控制流
if constexpr
正如本章开头出,我们知道了 C++11 引入了 constexpr
关键字,它将表达式或函数编译为常量结果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高?C++17 将 constexpr
这个关键字引入到 if
语句中,允许在代码中声明常量表达式的判断条件,考虑下面的代码:
#include |
---|
在编译时,实际代码就会表现为如下:
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;} |
---|
2.5 模板
外部模板
传统 C++ 中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板的实例化。
为此,C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能够显式的通知编译器何时进行模板的实例化:
template class std::vector |
---|
类型别名模板
在了解类型别名模板之前,需要理解『模板』和『类型』之间的不同。仔细体会这句话:模板是用来产生类型的。在传统 C++ 中,typedef
可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。例如:
template |
---|
C++11 使用 using
引入了下面这种形式的写法,并且同时支持对传统 typedef
相同的功效:
通常我们使用
typedef
定义别名的语法是:typedef 原名称 新名称;
,但是对函数指针等别名的定义语法却不相同,这通常给直接阅读造成了一定程度的困难。
typedef int (process)(void );using NewProcess = int()(void );template |
---|