2.语言可用性的强化
2.1 常量
nullptr
问题:C++ 不允许直接将 void * 隐式转换到其他类型。但如果编译器尝试把 NULL 定义为 ((void*)0), 那么在下面这句代码中:
没有了 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
如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。
#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 在编译期就应该是一个常量表达式。
2.2 变量及其初始化
if/switch 变量声明强化:
在判断条件之前能加上临时变量的初始化
if (初始化语句; 条件)
switch (初始化语句; 条件)
初始化列表:
问题:在传统 C++ 中, 不同的对象有着不同的初始化方法,例如普通数组、POD (Plain Old Data,即没有构造、析构和虚函 数的类或结构体)类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始 化,要么需要通过拷贝构造、要么就需要使用 () 进行。这些不同方法都针对各自对象,不能通用。
解决:C++11首先把初始化列表的概念绑定到了类型上,并将其称之为std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表。
MagicFoo(std::initializer_list<int> list) {
for (std::initializer_list<int>::iterator it = list.begin();
it != list.end(); ++it) vec.push_back(*it);
}
结构化绑定:
结构化绑定提供了类似其他语言中提供的多返回值的功能。
问题:C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型。
解决:更新之后,可以这样写。
#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;
}
2.3 类型推导
auto
一个最常见的例子就是迭代器。
// 由于 cbegin() 将返回 vector<int>::const_iterator 所以 itr 也应该是 vector<int>::const_iterator 类型
for(vector<int>::const_iterator it = vec.cbegin(); itr != vec.cend(); ++it)
而有了 auto 之后可以:
for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it)
注意:auto 不能用于函数传参,不能用于推导数组类型。
decltype
是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 typeof 很相似:
decltype(表达式)
if (std::is_same<decltype(x), int>::value)
std::cout << "type x == int" << std::endl;
尾返回类型推导
利用 auto 关键 字将返回类型后置
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}
decltype(auto)
主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的 指定 decltype 的参数表达式。
2.4 控制流
if constexpr
将 constexpr 这个关键字引入到 if 语句中,允许在代码中声明常量表达式的判断条件,能让程序效率更高。
#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 print_type_info(const int& t) {
return t + 1;
}
double print_type_info(const double& t) {
return t + 0.001;
}
区间 for 迭代
2.5 模板
模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动 态服务,进而大幅优化运行期的性能。
外部模板
问题:只要在每个编译单元(文件)中 编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。 并且,我们没有办法通知编译器不要触发模板的实例化。
解决:引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能 够显式的通知编译器何时进行模板的实例化:
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; // 不在该当前编译文件中实例化模板
尖括号 “>”
问题:在传统 C++ 的编译器中,>> 一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌 套模板的代码。
解决: C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译。
类型别名模板
问题:typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。
解决:C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效。通常是 typedef 原名称 新名称;,但是对函数指针等别名的定义语法却不相同,这通常给直接阅读造成了一定程度的困难。
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++11 中提供了一种便利,可以指定模板的默认参数
// T 和 U 后面的 = int 就是加的
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
变长参数模板
问题:无论是类模板 还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数。
解决: C++11 加入了新的表示方 法,允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。
如果不希望产生的模板参数个数为 0,可以手动的定义至少一个模板参数:
template<typename Require, typename... Args> class Magic;
除了在模板参数中能使用 ... 表示不定长模板参数外, 函数参数也使用同样的表示法代表不定长参数。
对参数进行解包,首先,我们可以使用 sizeof... 来计算参数的个数,
template<typename... Ts> void magic(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}
有两种经典的处理手法:
1. 递归模板函数
#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...);
}
2. 变参模板展开
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. 初始化列表展开
递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。
template<typename T, typename... Ts> auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
// 为了避免编译器警告,我们可以将 std::initializer_list 显式的转为 void
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}
使用了C++11 中提供的初始化列表以及 Lambda 表达式。
折叠表达式
将变长参数这种特性进一步带给了表达式
template<typename ... T> auto sum(T ... t) {
return (t + ...);
}
非类型模板参数推导
可以 用auto 关键字,让编译器辅助完成具体类型的推导。
2.6 面向对象
委托构造
构造函数可以在同一个类中一个构造函数调用另一个构造函数
#include <iostream> class Base {
public: int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = value;
}
};
继承构造
利用using关键字引入了继承构造
class Subclass : public Base {
public: using Base::Base; // 继承构造
};
显式虚函数重载
问题:在传统 C++ 中,经常容易发生意外重载虚函数的事情,例如
struct Base {
virtual void foo();
};
struct SubClass: Base {
void foo();
};
SubClass::foo 可能并不是程序员尝试重载虚函数,只是恰好加入了一个具有相同名字的函数。另 一个可能的情形是,当基类的虚函数被删除后,子类拥有旧的函数就不再重载该虚拟函数并摇身一变成 为了一个普通的类方法,这将造成灾难性的后果。
解决:引入了override和final。
override 当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函 数是否存在这样的虚函数,否则将无法通过编译。
struct Base {
virtual void foo(int);
};
struct SubClass: Base {
virtual void foo(int) override; // 合法
virtual void foo(float) override; // 非法, 父类没有此虚函数
};
final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。
struct Base {
virtual void foo() final;
};
struct SubClass1 final: Base {
}; // 合法
struct SubClass2 : SubClass1 {
}; // 非法, SubClass1 已 final
struct SubClass3: Base {
void foo(); // 非法, foo 已 final
};
显式禁用默认函数
传统C++中,
如果程序员没有提供,编译器会默认为对象生成默认构造函数、复制构造、赋值
算符以及析构函数。
另外,
C++
也为所有类定义了诸如
new delete
这样的运算符。当程序员有需要时,
可以重载这部分函数。
这就引发了一些需求:无法精确控制默认函数的生成行为。例如禁止类的拷贝时,必须将复制构造
函数与赋值算符声明为
private
。尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式。
并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。若用户定义了任何构造函
数,编译器将不再生成默认构造函数,但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬。
C++11
提供了上述需求的解决方案,允许显式的声明采用或拒绝编译器自带的函数。例如:
class Magic {
public:
Magic() = default; // 显式声明使用编译器生成的构造
Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
Magic(int magic_number);
}
强枚举类型
在传统
C++
中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类
型可以进行直接的比较(虽然编译器给出了检查,但并非所有),
甚至同一个命名空间中的不同枚举类型
的枚举值名字不能相同
,这通常不是我们希望看到的结果。
C++11
引入了枚举类(
enumeration class),
并使用
enum class
的语法进行声明:
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};
这样定义的枚举实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数
字进行比较,更不可能对不同的枚举类型的枚举值进行比较。但相同枚举值之间如果指定的值相同,那
么可以进行比较:
if (new_enum::value3 == new_enum::value4) {
// 会输出
std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
}
在这个语法中,枚举类型后面使用了冒号及类型关键字来指定枚举中枚举值的类型,这使得我们能
够为枚举赋值(未指定时将默认使用
int
)。
而我们希望获得枚举值的值时,将必须显式的进行类型转换,不过我们可以通过重载
<<
这个算符来
进行输出,可以收藏下面这个代码段:
#include <iostream>
template<typename T>
std::ostream& operator<<(typename std::enable_if<std::is_enum<T>::value, std::ostream>::type& stream, {
return stream << static_cast<typename std::underlying_type<T>::type>(e);
}
这时,下面的代码将能够被编译:
std::cout << new_enum::value3 << std::endl