本章首先复习前面介绍过的C++11功能,然后介绍如下主题:
- 移动语义和右值引用。
- Lambda 表达式。
- 包装器模板 function。
- 可变参数模板。
本章重点介绍 C++11 对 C++ 所做的改进。本书前面介绍过多项 C++11 功能,本章首先复习这些功能,并详细介绍其他一些功能。然后,指出一些超出了本书范围的 C++11 新增功能(考虑到 C++11 草案的篇幅比 C++98 长 98%,本书无法全面介绍)。最后,将简要地探讨 BOOST 库。
复习前面介绍过的 C++11 功能
本书前面介绍过很多 C++11 改进,但您现在可能忘了,本节简要地复习这些改进。
新类型
C++11 新增了类型 long long 和 unsigned long long,以支持 64 位(或更宽)的整型;新增了类型 char16_t 和 char32_t,以支持 16 位和 32 位的字符表示;还新增了“原始”字符串。第3章讨论了这些新增的类型。
统一的初始化
C++11 扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象)。使用初始化列表时,可添加等号(=),也可不添加:
int x = {5};
double y {2.75};
short quar[5] {4, 5, 2, 76, 1};
另外,列表初始化语法也可用于 new 表达式中:
int * ar = new int [4] {2, 4, 6, 7}; // C++11
创建对象时,也可使用大括号(而不是园括号)括起的列表来调用构造函数:
class Stump {
private:
int roots;
double weight;
public:
Stump(int r, double w) : roots(r), weight(w) {}
};
Stump s1(3,15.6); // old style
Stump s2{5, 43.4}; // C++11
Stump s3 = { 4, 32.1}; // C++ 11
然而,如果类有将模板 std::initializer_list 作为参数的构造函数,则只有该构造函数可以使用列表初始化形式。
-
缩窄
初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。常规初始化允许程序执行可能没有意义的操作:
char c1 = 1.57e27; // double-to-char, undefined behavior char c2 = 459585821; // int-to-char, out of range, compile-time error
然而,如果使用初始化列表语法,编译器将禁止进行这样的类型转换,即将值存储到比它“窄”的变量中:
char c1 {1.57e27}; // double-to-char, compile-time error char c2 = {459585821}; // int-to-char, out of range, compile-time error
但允许转换为更宽的类型。
另外,只要值在较窄类型的取值范围内,将其转换为较窄的类型也是允许的:
char c1 {66}; // int-to-char, in range, allowed double c2 = {66}; // int-to-double, allowed
-
initializer_list
C++11 提供了模板类 initializer_list,可将其用作构造函数的参数,这在第16章讨论过。如果类有接受 initializer_list 作为参数,则初始化列表就只能用于该构造函数。列表中的元素必须是同一种类型或可转换为同一种类型。STL 容器提供了将 initializer_list 作为参数的构造函数:
vector<int> a1(10); // uninitialized vector with 10 elements vector<int> a2{10}; // initializer-list, a2 has 1 element set to 10 vector<int> a3{4,6,1}; // 3 elements set to 4,6,1
头文件 initializer_list 提供了对模板类 initializer_list 的支持。这个类包含成员函数 begin() 和 end(),可用于获悉列表的范围。除用于构造函数外,还可将 initializer_list 用作常规函数的参数:
#include<initializer_list> double sum(std::initializer_list<double> il); int main() { double total = sum({2.5,3.1,4}); // 4 converted to 4.0 ... } double sum(std::initializer_list<double> il) { double tot = 0; for (auto p = il.begin(); p!=il.end(); p++){ tot += *p; } return tot; }
声明
C++11 提供了多种简化声明的功能,尤其在使用模板时。
-
auto
以前,关键字 auto 是一个存储类型说明符,C++11 将其用于实现自动类型推断。这要求进行显式初始化,让编译器能够将变量的类型设置为初始值的类型:auto maton = 112; // maton is type int auto pt = &maton; // pt is type int * double fm(double, int); auto pf = fm; // pf is type double(*) (double, int)
关键字 auto 还可以简化模板声明。例如,如果 il 是一个 std::initializer_list<double>对象,则可将下述代码:
for(std::intializer_list<double>::iterator p = il.begin(); p != il.end(); p++ )
替换为如下代码:
for (auto p = il.begin(); p != il.end(); p++ )
-
decltype
关键字 decltype 将变量的类型声明为表达式指定的类型。下面的语句含义是,让 y 的类型与 x 相同,其中 x 是一个表达式:
decltype(x) y;
下面是几个示例:
double x; int n; decltype(x*n) q; // q same type as x*n, i.e., double decltype(&x) pd; // pd same as &x, i.e., double *
这在定义模板时特别有用,因为只有等到模板被实例化时才能确定类型:
template<typename T, typename U> void ef(T t, U u) { decltype(T*U) tu; ... }
其中 tu 将为表达式 T*U 的类型,这里假定定义了运算 T*U。例如,如果 T 为 char,U 为 short,则 tu 将为 int,这是由整型算术自动执行整型提升导致的。
decltype 的工作原理比auto 复杂,根据使用的表达式,指定的类型可以为引用和 const。下面是几个示例:
int j = 3; int &k = j; const int &n = j; decltype(n) i1; // i1 type const int & decltype(j) i2; // i2 type int decltype((j)) i3; // i3 type int & decltype(k + 1) i4; // i4 type int
有关导致上述结果的规则的详细信息,请参阅第8章。
-
返回类型后置
C++11 新增了一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型:
double f1(double, int); // traditional syntax auto f2(double, int) -> double; // new syntax, return type is double
就常规函数的可读性而言,这种新语法好像是倒退,但让您能够使用 decltype 来指定模板函数的返回类型:
template<typename T, typename U> auto eff(T t, U u) -> decltype(T*U) { ... }
这里解决的问题是,在编译器遇到 eff 的参数列表前,T 和 U 还不在作用域内,因此必须在参数列表后使用 decltype。这种新语法使得能够这样做。
-
模板别名: using =
对于冗长或复杂的标识符,如果能够创建其别名将很方便。以前,C++ 为此提供了 typedef:typedef std::vector<std::string>::iterator itType;
C++ 提供了另一种创建别名的语法,这在第14章讨论过:
using itType = std::vector<std::string>::iterator;
差别在于,新语法也可用于模板部分具体化,但 typedef不能:
template<typename T> using arr12 = std::array<T, 12>; // template for multiple aliases
上述语句具体化模板 array<T, int> (将参数 int 设置为 12)。例如,对于下述声明:
std::array<double, 12> a1; std::array<std::string, 12> a1;
可将它们替换为如下声明:
arr12<double> a1; arr12<std::string> a2;
-
nullptr
空指针是不会指向有效数据的指针。以前,C++ 在源代码中使用 0 表示这种指针,但内部表示可能不同。这带来了一些问题,因为这使得 0 既可以表示指针常量,又可表示整型常量。正如第 12 章讨论的,C++11 新增了关键字 nullptr,用于表示空指针;它是指针类型,不能转换为整数类型。为向后兼容,C++11 仍允许使用 0 来表示空指针,因此表达式 nullptr == 0 为 true,但使用 nullptr 而不是 0 提供了更高的类型安全。例如,可将 0 传递给接受 int 参数的函数,但如果您试图将 nullptr 传递给这样的函数,编译器将此视为错误。因此,处于清晰和安全考虑,请使用 nullptr——如果您的编译器支持它。
智能指针
如果在程序中使用 new 从堆(自由存储区)分配内存,等到不再需要时,应使用 delete 将其释放。C++ 引入了智能指针 auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用 STL时)表明,需要有更精致的机制。基于程序员的编程体验和 BOOST 库提供的解决方案,C++11 摒弃了 auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr 和 weak_ptr,第 16 章讨论了前两种。
所有新增的智能指针都能与 STL 容器和移动语义协同工作。
异常规范方面的修改
以前,C++ 提供了一种语法,可用于指出函数可能引发哪些异常(参见第15章):
void f501(int) throw(bad_dog); // can throw type bad_dog exception
void f733(long long) throw(); // doesn't throw an exception
与 auto_ptr 一样, C++ 编程社区的集体经验表明,异常规范的效果没有预期的好。因此,C++11 摒弃了异常规范。然而,标准委员会认为,指出函数不会引发异常有一定的价值,他们为此添加了关键字 noexcept:
void f875(short, short) noexcept; // doesn't throw an exception
作用域内枚举
传统的 C++ 枚举提供了一种创建名称常量的方式,但其类型检查相当低级。另外,枚举名的作用域为枚举定义所属的作用域,这意味着如果在同一个作用域内定义两个枚举,它们的枚举成员不能同名。最后,枚举可能不是可完全移植的,因为不同的实现可能选择不同的底层类型。为解决这些问题,C++新增了一种枚举。这种枚举使用 class 或 struct 定义:
enum Old1 {yes, not, maybe}; // traditional form
enum class New1 { never, sometime, often, always }; // new form
enum struct New2 { never, lever, sever }; // new form
新枚举要求进行显式限定,以免发生名称冲突。因此,引用特定枚举时,需要使用 New1::never 和 New2::never 等。更详细的信息请参阅第10章。
对类的修改
为简化和扩展类设计,C++11 作了多项改进。这包括允许构造函数被继承和彼此调用、更佳的方法访问控制方式以及移动构造函数和移动赋值运算符,这些都将在本章介绍。下面先来复习本书前面介绍过的改进。
-
显式转换运算符
有趣的是,C++ 很早就支持对象自动转换。但随着编程经验的积累,程序员逐渐认识到,自动类型转换可能导致意外转换的问题。为解决这种问题,C++ 引入了关键字 explicit,以禁止单参数构造函数导致的自动转换:
class Plebe { Plebe(int); // automatic int-to-plebe conversion explicit Plebe(double); // requires explicit use ... }; Plebe a, b; a = 5; // implicit concersion, call Plebe(5) b = 0.5; // not allowed b = Plebe(0.5); // explicit conversion
C++11 扩展了 explicit 的这种用法,使得可对转换函数做类似的处理(参见第11章):
class Plebe{ ... // conversion functions operator int() const; explicit operator double() const; ... }; Plebe a, b; int n = a; // int-to-Plebe automatic conversion double x = b; // not allowed x = double(b); // explicit conversion, allowed
-
类内成员初始化
很多首次使用 C++ 的用户都会问,为何不能在类定义中初始化成员?现在可以这样做了,其语法类似于下面这样:class Session { int mem1 = 10; // in-class initialization double mem2 {1966.54}; // in-class initialization short mem3; public: Session(){} // #1 Session(short s) : mem3(s) {} // #2 Session(int n, double d, short s) : mem1(n), mem2(d), mem3(s) {} // #3 ... };
可使用等号或大括号版本的初始化,但不能使用圆括号版本的初始化。其结果与给前两个构造函数提供成员初始化列表,并指定 mem1 和 mem2 的值相同:
Session() : mem1(10), mem2(1966.54) {} Session(short s) : mem1(10), mem2(1966.54), mem3(s) {}
通过使用类内初始化,可避免在构造函数中编写重复的代码,从而降低了程序员的工作量、厌倦情绪和出错的机会。
如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖,因此第三个构造函数覆盖了类内成员初始化。
模板和 STL 方面的修改
为改善模板和标准模板库的可用性,C++11 做了多个改进;有些是库本身,有些与易用性相关。本章前面提高了模板别名和适用于 STL 的智能指针。
-
基于范围的 for 循环
对于内置数组以及包含方法 begin() 和 end() 的类(如 std::string) 和 STL 容器,基于范围的 for 循环(第5章和第16章讨论过)可简化为它们编写循环的工作。这种循环对数组或容器中的每个元素执行指定的操作:
double prices[5] = { 4.99, 10.99, 6.87, 7.99, 8.49 }; for (double x : prices) { std::cout << x << std::endl; }
其中,x 将依次为 prices 中每个元素的值。x 的类型应与数组元素的类型匹配。一种更容易、更安全的方式是,使用 auto 来声明 x,这样编译器将根据 prices 声明中的信息来推断 x 的类型:
double prices[5] = { 4.99, 10.99, 6.87, 7.99, 8.49 }; for (auto x : prices) { std::cout << x << std::endl; }
如果要在循环中修改数组或容器的每个元素,可使用引用类型:
std::vector<int> vi(6); for (auto & x : vi) // use a reference if loop alters contents x = std::rand();
-
新的 STL 容器
C++11 新增了 STL 容器 foward_list、unordered_map、unordered_multimap、unordered_set 和 unordered_multiset(参见第16章)。容器 forward_list 是一种单向链表,只能沿一个方向遍历;与双向链接的 list 容器相比,它更简单,在占用存储空间方面更经济。其他四种容器都是使用哈希表实现的。
C++11 还新增了模板 array(这在第4和第16章讨论过)。要实例化这种模板,可指定元素类型和固定的元素数:
std::array<int, 360> ar; // array of 360 ints
这个模板类没有满足所有的常规模板需求。例如,由于长度固定,您不能使用任何修改容器大小的方法,如 put_back()。但 array 确实有方法 begin() 和 end(),这让您能够对 array 对象使用众多基于范围的 STL 算法。
-
新的STL方法
C++11新增了 STL 方法 cbegin() 和 cend()。与 begin() 和 end() 一样,这些新方法也返回一个迭代器,指向容器的第一个元素和最后一个元素的后面,因此可用于指定包含全部元素的区间。另外,这些新方法将元素视为 const。与此类似,crbegin() 和 crend() 是 rbegin() 和 rend() 的 const 版本。
更重要的是,除传统的复制构造函数和常规赋值运算符外,STL 容器现在还有移动构造函数和移动赋值运算符。移动语义将在本章后面介绍。
-
valarray 升级
模板 valarray 是独立于 STL 开发的,其最初的设计导致无法将基于范围的 STL 算法用于 valarray 对象。C++11 添加了两个函数(begin() 和 end() ),它们都接受 valarray 作为参数,并返回迭代器,这些迭代器分别指向 valarray 对象的第一个元素和最后一个元素后面。这让您能够将基于范围的 STL 算法用于 valarray。
-
摒弃 export
C++98 新增了关键字 export,旨在提供一种途径,让程序员能够将模板定义放在接口和实现文件中,其中前者包含原型和模板声明,而后者包含模板函数和方法的定义。实践证明这不现实,因此 C++11 终止了这种用法,但仍保留了关键字 export,供以后使用。
-
尖括号
为避免与运算符 >> 混淆,C++ 要求在声明嵌套模板时使用空格将尖括号分开:
std::vector<std::list<int> > v1; // >> not ok
C++ 11不再这样要求:
std::vector<std::list<int>> v1; // >> ok in C++11
右值引用
传统的 C++ 引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符 const 的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址:
int n;
int * pt = new int;
const int b = 101; // can't assign to b, but &b is valid
int & rn = n; // n identifies datum at address &n
int & rt = *pt; // *pt identifies datum at address pt
const int & rb = b; // b idendifies const datum at address &b
C++11 新增了右值引用(这在第8章讨论过),这是使用&&表示的。右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如 x+y 等表达式以及返回值的函数(条件是该函数返回的不是引用):
int x = 10;
int y = 20;
int && r1 = 13;
int && r2 = x + y;
double && r3 = std::sqrt(2.0);
注意:r2 关联到的是当时计算 x + y 得到的结果。也就是说,r2 关联到的是 23,即使以后修改了 x 或 y,也不会影响到 r2。
有趣的是,将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符 & 用于 13,但可将其用于 r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。
下面的程序是一个简短的示例,演示了上述有关右值引用的要点。
// rvref.cpp -- simple uses of rvalue references
#include<iostream>
inline double f(double tf) {
return 5.0 * (tf-32.0)/9.0;
}
int main(){
using namespace std;
double tc = 21.5;
double && rd1 = 7.07;
double && rd2 = 1.8 * tc + 32.0;
double && rd3 = f(rd2);
cout << " tc value and address: " << tc << ", " << &tc << endl;
cout << "rd1 value and address: " << rd1 << ", " << &rd1 << endl;
cout << "rd2 value and address: " << rd2 << ", " << &rd2 << endl;
cout << "rd3 value and address: " << rd3 << ", " << &rd3 << endl;
cin.get();
return 0;
}
该程序的输出如下:
tc value and address: 21.5, 0x7ffdd471ca40
rd1 value and address: 7.07, 0x7ffdd471ca30
rd2 value and address: 70.7, 0x7ffdd471ca20
rd3 value and address: 21.5, 0x7ffdd471ca10
引入右值引用的主要目的之一是实现移动语义,这是本章将讨论的下一个主题。