C++11 新特性概述

本文概述了C++11在C++98基础上主要的一些新特性,内容主要转自这里

一、关键字和新语法

1.1 auto关键字

C++11以前,auto是用来表明变量是放在自动存储区(栈区)的。C++11开始,废弃了这个功能,转而用来进行类型判断,编译器会根据上下文来判断auto变量的类型。auto作为函数返回值时,只能用于定义函数,不能用于声明函数。下面的写法是可行的。也就是说auto附近必须有上下文可以推断类型。

1
2
3
4
5
6
7
#pragma once
class Test {
public:
auto TestWork(int a, int b) {
return a + b;
}
};

1.2 nullptr关键字

以前,我们都是用NULL 或者0来表示空指针,但是由于NULL可以隐式转换为整型,在函数调用时可能会出现问题。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test {
public:
void TestWork(int index) {
std::cout << "TestWork 1" << std::endl;
}
void TestWork(int* index) {
std::cout << "TestWork 2" << std::endl;
}
};
int main() {
Test test;
test.TestWork(NULL);
test.TestWork(nullptr);
}

上面的代码,用NULL调用时,会先匹配整型,这不是我们希望的。而使用nullptr就可以规避这个问题,因为nullptr不允许隐式转换为整型,只允许隐式转换为bool型(false)和任意指针类型(空指针)。

1.3 基于范围的for循环

C++11 引入了一种更为简单的for语句,这种for语句可以很方便的遍历容器或其他序列的所有元素。

1
2
3
4
5
6
int main() {
int numbers[] = { 1,2,3,4,5 };
std::cout << "numbers:" << std::endl;
for (auto number : numbers) {
std::cout << number << std::endl;
}

1.4 大括号初始化器

C++11中全面加入了列表初始化的功能,包括对vectormap,值类型,struct等等都可以使用列表初始化,还可以在函数中返回一个花括号括起来的列表,而在这之前我们只能对数组进行列表初始化。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//数组列表初始化
int xx[5]={1,2,3,4,5};
int yy[]={6,7,8,9,0};
//值类型进行初始化
int a{10};
int b={10};
int c={10.123}; // 编译器报错,g++ 5.3.1当列表初始化用于值类型的时候,如果有精度损失,编译器会报错。
//列表初始化还可以用结构体
typedef struct Str{
int x;
int y;
} Str;
Str s = {10,20};
//列表初始化类,必须是public成员,如果含有私有成员会失败
class Cls {
public:
int x;
int y;
};
Cls c = {10,20};
//vector不仅可以使用列表初始化,还可以使用列表进行赋值,数组不能用列表赋值
vector<int>v1={1,2,3,4,5,6,7,8,9}; // 初始化
vector<int>v2;
v2={3,4,5,6,7}; //赋值
//map列表初始化
map<string ,int> m = {
{"x",1},
{"y",2},
{"z",3}
};
//用函数返回初始化列表只展示关键代码,相关头文件自行添加
//同理结构体,类,map的返回也可以使用初始化列表返回
vector<int> getVector() {
return {1,2,3,4,5};
}
int main() {
vector<int> v = getVector();
cout<<v[0]<<v[1]<<v.size()<<endl;
return 0 ;
}

1.5 long long类型(长整型)

long long 类型实际上没有在C++ 98中存在,而之后被C99标准收录,其实现在市面上大多数编译器是支持 long long 的,但是这个类型正式成为C++的标准类型是在C++11中。标准要求long long至少是64位也就是8个字节。一个字面常量使用LL后缀表示long long类型,使用ULL后缀表示unsigned long long 类型。

1.6 char16_t 与 char32_t 类型(宽字符型)

为了表示更宽的字符集,C++11提供了2字节和4字节字符整型

1.7 raw 字符串 R”( )”

C++11提供了写字符串常量的方式,R"()"内的内容会原原本本的显示出来,不会有转义的问题,正则表达式这种字符串很友好。

1.8 using 类型别名

类型别名其实早在C语言中就有了,一般情况下我们使用关键字typedef来声明一个类型的别名,在C++11中增加了另一种声明类型别名的方法就是使用using关键字,using关键字在C++11以前一般用来引用命名空间。

1
2
3
4
5
typedef int INT; // 右侧符号代表左侧
using INT2 = int; // 左侧符号代表右侧
INT a = 20;
INT2 b = 30;

1.9 decltype类型指示符

有时候会有这样的需求,我们需要知道一个表达式的类型,并使用该类型去定义一个变量,例如:

1
2
3
int a = 10;
int b = 20;
auto c = a + b; // OK a+b的类型是int,此时c的类型是int,并且c的值是 a+b

auto可以解决部分问题,例如我们定义的变量的类型就是表达式 a+b 的类型,但是如果我们仅仅需要定义一个与表达式 a+b 的类型相同的变量,但是我们又不希望将表达式a+b的值赋值给刚刚定义的变量,我们希望赋另外一个值或者是仅仅定义变量而不赋值呢。 这就需要用到C++11 提供的另一个类型说明符 decltype了。decltype作用于一个表达式,并且返回该表达式的类型,在此过程中编译器分析表达式的类型,并不会计算表达式的值。例如

1
2
3
int a = 10;
int b = 20;
decltype(a+b) c = 50; // OK c的类型就是 a+b 的类型int

对于引用类型decltype有一些特别的地方:

1
2
3
4
int a = 20 ;
int &b = a;
decltype(b) c ; // Error c是引用类型必须赋值
decltype(b) d = a; // OK d是引用类型,指向a

可以看到decltype如果作用于一个引用类型,其得到的还是一个引用类型。我们知道一个引用类型在使用的时候一般会当作其关联的那个变量的同义词处理,例如如果使用 cout<<b<<endl; 其中b实际上相当于a,但是decltype作用于引用类型的时候会保留引用性质。

如果一个表达式是一个解指针引用的操作,decltype得到的也是一个引用类型:

1
2
3
4
5
int a = 20 ;
int *p = &a;
decltype(*p) c = a; // c的类型是int&
c = 50;
cout<<a<<endl; // 输出50

当decltype作用于一个变量的时候,变量加不加括号是有区别的,例如:

1
2
3
int a = 20;
decltype(a) b = 30; //ok b的类型是 int
decltype((a)) c = a ; // ok c的类型是int& 其关联变量 a

加上括号之后编译器会把(a)当作是一个表达式处理,而变量是一种可以作为赋值语句左值的表达式,所以会解释成引用类型

二、智能指针与内存管理

C++11 从boost 库中引入了share_ptrunique_ptrweak_ptr做智能指针,进行内存管理。

C++11以前,有auto_ptr,但是一个对象只能有一个auto_ptr持有(防止重复析构所引用的对象),它利用的是栈内存释放规则,回收auto_ptr时,调用它的析构函数,在析构函数里调用所引用对象的析构。

C++11摒弃了auto_ptr,使用另外三个来做动态内存管理。智能指针是为了解决堆内存管理而存在的。要是用智能指针,必须包含memory头文件,这些智能指针的定义都在这个文件中:

1
#include <memory>

智能指针重载了解除引用运算符 * 使得他们可以像普通指针一样来获得所引用的对象的内容。

1
2
operator * 接触引用
T* get() 获得对象的地址

2.1 unique_ptr

unique_ptr作为auto_ptr的替代品,比auto_ptr更安全,因为它不允许赋值,也就是不能把对象所有权转移,会抛出编译错误。如:

1
2
3
unique_ptr<string> p1(new string("hello")); // #1
unique_ptr<string> p2 = p1; // #2
p2 = p1; // #3

#2和#3都是违法的,不允许使用复制构造函数和赋值运算符,这种特性是通过C++11的移动构造函数和右值引用实现的。

而在auto_ptr中,上面的语法都是正确的:

1
2
3
4
auto_ptr<string> p1(new string("string"));
auto_ptr<string> p2 = p1;
p2 = p1;
int n = p1.size(); // 错误,p1已经丧失所有权

p2会获得堆变量new string("string")所有权,如果再次使用p1,程序在运行时会崩溃,这样很不安全,于是引入了unique_ptr来禁止引用转移。

2.2 shared_ptr

shared_ptr允许多个shadred_ptr引用同一个对象,和unique_ptr相反。
shared_ptr允许多个指针指向同一个对象,unique_ptr则独占所指向的对象,我们主要说明shared_ptr的使用。通过使用make_shared<type>()函数产生智能指针对象。

1
2
shared_ptr<int> p = make_shared<int>(40); // p指向一个值为40的int对象
shared_ptr<string> p2 = make_shared<string>(10,'c'); //指向值为'cccccccccc'的string对象

make_shared<type>()函数中传递的值要与对应的type的构造函数相匹配,实际上应该就是直接调用的对应type的构造函数。

我们可以使用new初始化的指针来初始化智能指针:

1
2
share_ptr<int> p (new int(40));
p.get(); // 使用share_ptr<type>的get()函数来获得其关联的原始指针。

shared_ptr对象在离开其作用域(例如一个函数体),会自动释放其关联的指针指向的动态内存,就像局部变量那样。另外多个shared_ptr可以指向一个对象,当最后一个shared_ptr对象销毁的时候才会销毁其关联的那个对象的动态内存。这里使用了引用记数。

有个地方需要注意,当删除一个智能指针时,并不影响其它两个智能指针的继续使用。因为该片内存添加了一个引用计数,每shared_ptr一次,引用计数+1;每次调用析构函数,引用计数减一。直到最后一个智能指针删除,才会释放内存。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include<iostream>
using namespace std;
template<typename T>
class smartptr{
public:
smartptr(T* ptr=0);
smartptr(const smartptr& src);
smartptr& operator= (const smartptr& src);
T& operator* (){
if(_ptr){
return *_ptr;
}
}
T* operator-> (){
if(_ptr){
return _ptr;
}
}
~smartptr();
void print_data(){//不能重载运算符,不然会多算一个指针
cout<<(*_ptr)<<" "<<*_referece_count<<endl;
}
private:
size_t* _referece_count;//引用计数
T* _ptr;
void releasecount();
};
template<typename T>
void smartptr<T>::releasecount(){
if(_ptr){
//--(*_referece_count);
if(--(*_referece_count)==0){
delete _ptr;
delete _referece_count;
}
}
}
template<typename T>
smartptr<T>::smartptr(T* ptr):_ptr(ptr),_referece_count(new size_t){
if(ptr){
*_referece_count=1;
}
else{
*_referece_count=0;
}
}
template<typename T>
smartptr<T>::smartptr(const smartptr& src){
if(this!=&src){
_ptr=src._ptr;
_referece_count = src._referece_count;
(*_referece_count)++;
}
}
template<typename T>
smartptr<T>& smartptr<T>::operator= (const smartptr& src){
if(_ptr == src._ptr){
return *this;
}
releasecount();
_ptr = src._ptr;
_referece_count=src._referece_count;
(*_referece_count)++;
return *this;
}
template<typename T>
smartptr<T>::~smartptr(){
if(--*(_referece_count)==0){
delete _ptr;
delete _referece_count;
}
}
int main() {
smartptr<char>cp1(new char('a'));
cp1.print_data();
smartptr<char>cp2(cp1);
cp2.print_data();
cp1.print_data();
smartptr<char>cp3;
cp3 = cp2;
cp2.print_data();
smartptr<char>cp4(new char('b'));
cp4.print_data();
}

三、新增容器

3.1 std::array

用于创建位于栈区的定长数组,和数组的区别是增加了迭代器。

1
2
3
4
5
6
7
8
9
10
11
#include <array>
int main() {
std::array<int, 4> arrayDemo = { 1,2,3,4 };
std::cout << "arrayDemo:" << std::endl;
for (auto itor : arrayDemo) {
std::cout << itor << std::endl;
}
int arrayDemoSize = sizeof(arrayDemo);
std::cout << "arrayDemo size:" << arrayDemoSize << std::endl;
return 0;
}

3.2 std::forward_list 单向链表

std::forward_list为C++新增的线性表,与list区别在于它是单向链表。我们在学习数据结构的时候都知道,链表在对数据进行插入和删除是比顺序存储的线性表有优势,因此在插入和删除操作频繁的应用场景中,使用listforward_list比使用arrayvectordeque效率要高很多。

3.3 std::unordered_map

std::unordered_mapstd::map用法基本差不多,但STL在内部实现上有很大不同,std::map使用的数据结构为二叉树,而std::unordered_map内部是哈希表的实现方式,哈希map理论上查找效率为O(1)。但在存储效率上,哈希map需要增加哈希表的内存开销。

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
#include <iostream>
#include <string>
#include <unordered_map>
int main()
{
std::unordered_map<std::string, std::string> mymap =
{
{ "house","maison" },
{ "apple","pomme" },
{ "tree","arbre" },
{ "book","livre" },
{ "door","porte" },
{ "grapefruit","pamplemousse" }
};
unsigned n = mymap.bucket_count();
std::cout << "mymap has " << n << " buckets.\n";
for (unsigned i = 0; i<n; ++i)
{
std::cout << "bucket #" << i << " contains: ";
for (auto it = mymap.begin(i); it != mymap.end(i); ++it)
std::cout << "[" << it->first << ":" << it->second << "] ";
std::cout << "\n";
}
return 0;
}

3.4 std::unordered_set

std::unordered_set的数据存储结构也是哈希表的方式结构,除此之外,std::unordered_set在插入时不会自动排序,这都是std::set表现不同的地方。

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
#include <iostream>
#include <string>
#include <unordered_set>
#include <set>
int main() {
std::unordered_set<int> unorder_set;
unorder_set.insert(7);
unorder_set.insert(5);
unorder_set.insert(3);
unorder_set.insert(4);
unorder_set.insert(6);
std::cout << "unorder_set:" << std::endl;
for (auto itor : unorder_set) {
std::cout << itor << std::endl;
}
std::set<int> set;
set.insert(7);
set.insert(5);
set.insert(3);
set.insert(4);
set.insert(6);
std::cout << "set:" << std::endl;
for (auto itor : set) {
std::cout << itor << std::endl;
}
}

四、lambda表达式(匿名函数)

lambda表达式是C++11最重要也最常用的一个特性之一。lambda来源于函数式编程的概念,也是现代编程语言的一个特点。

4.1 函数式编程简介

定义:简单说,“函数式编程”是一种“编程范式”。它属于“结构化编程”的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。

特点:

  1. 函数是“第一等公民”,可以赋值给他其他变量,也可以做为参数,返回值。

  2. 只用“表达式”,不用“语句”。“表达式”是一个单纯的运算过程,总是有返回值;“语句”是执行某种操作,没有返回值。

  3. 没有副作用。函数保持独立,所有功能就是返回一个新的值,其他什么都不做,不修改外部变量的值。

  4. 引用透明。函数的运行不依赖于外部变量或“状态”,只依赖于输入的参数,只要参数相同,返回值就相同。

4.2 lambda 表达式

lambda表达式提供了一种匿名函数的实现方式。

1
[捕获列表](参数列表) -> 返回类型 {函数体}

其中,参数列表、返回类型可以省略,其余不可省略,如:

1
[x] {} ; // 可以,最起码得这样

lambda表达式的返回值由函数体的return决定,如果函数体内无return ,默认是void。只有被捕获的变量才能在函数体重访问到lambda表达式

1
[capture_block](parameters) mutable exception_specification->return_type{ body }

lambda表达式包含以下部分:

捕捉块(catpure block): 指定如何捕捉所在作用域中的变量,并供给lambda主体使用。

参数(parameter): (可选)lambda表达式使用的参数列表。只有在不使用任何参数,并且没有自定mutable、一个exception_specification 和一个return_type的情况下可以忽略该列表,返回类型在某些情况下也是可以忽略的,详见对return_type的说明:eg: [] {return 10;}

参数列表和普通函数的参数列表类似,区别如下:

  • 参数不能有默认值。
  • 不允许变长参数列表。
  • 不允许未命名的参数。

mutable:(可选)如果所在作用域的变量是通过值捕捉到,那么lambda表达式主体中可以使用这些变量的副本。这些副本默认标记为const,因此lambda表达式的主体不能修改这些副本的值。如果lambda表达式标记为mutable,那么这些副本则不是const,因此主体可以修改这些本地副本。

exception_specification:(可选)用于指定lambda可以抛出的异常。

return_type:(可选)返回值的类型。如果忽略了return_type,那么编译器会根据以下原则判断返回类型:
如果lambda表达式主体的形式为{return expression;},那么表达式return_type的类型为expression类型。
其他情况下的return_typevoid

下面的例子演示了如何创建一个lambda表达式并立即执行这个表达式。这行代码定义了一个没有返回值也没有任何参数的lambda表达式

注意:尾部的(),这对括号使得这个lambda表达式立即执行

1
[] {cout << "Hello from Lambda" << endl;} ();
1
2
string result = [](const string & str) -> string { return "Hello from " + str; }("second Lambda");
cout << "Result: " << result << endl;

输出如下:

1
Result: Hello from second Lambda

根据前面的描述,这个例子中的返回值可以忽略:

1
string result = [](const string & str){ return "Hello from " + str; }("second Lambda");

还可以保存lambda表达式的指针,并且通过函数指针执行这个lambda表达式。使用C++11的auto关键字可以轻松地做到这一点:

1
2
3
auto fn = [](const string& str) {return "hello from " + str; };
cout << fn("call 1") << endl;
cout << fn("call 2") << endl;

输出如下:

1
2
Hello from call 1
Hello from call 2

4.3 捕捉块详解

lambda表达式的方括号部分称为lambda捕捉块(capture block),在这里可以指定如何从所在作用域中捕捉变量。捕捉变量的意思是可以在lambda表达式主体中使用这个变量。有两种方式:

[=]:通过值捕捉所有变量
[&]:通过引用捕捉所有变量

指定空白的捕捉块[]表示不从所在作用域中捕捉变量。还可以酌情决定捕捉那些变量以及这些变量的捕捉方法,方法是指定一个捕捉列表,其中带有可选的默认捕捉选项。前缀为&的变量通过引用捕捉。不带前缀的变量通过值捕捉。默认捕捉应该是捕捉列表中的第一个元素,可以是=&

例如:
[&x] 只通过引用捕捉x,不捕捉其他变量。
[x] 只通过值捕捉x,不捕捉其他变量。
[=, &x,&y] 默认通过值捕捉,变量x和y例外,这两个变量通过引用捕捉。
[&, x] 默认通过引用捕捉,变量x例外,这个变量通过引用捕捉。
[&x, &y] 非法,因为标志符不允许重复。
[this] 捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。

通过引用捕捉变量的时候,一定保证当lambda表达式在执行的时候,这个引用还是可用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A {
public:
int i_ = 0;
void func(int x,int y) {
auto x1 = [] { return i_; }; //error,没有捕获外部变量
auto x2 = [=] { return i_ + x + y; }; //OK
auto x3 = [&] { return i_ + x + y; }; //OK
auto x4 = [this] { return i_; }; //OK
auto x5 = [this] { return i_ + x + y; }; //error,没有捕获x,y
auto x6 = [this, x, y] { return i_ + x + y; }; //OK
auto x7 = [this] { return i_++; }; //OK
}
};
int a=0 , b=1;
auto f1 = [] { return a; }; //error,没有捕获外部变量
auto f2 = [&] { return a++ }; //OK
auto f3 = [=] { return a; }; //OK
auto f4 = [=] {return a++; }; //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; }; //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); }; //OK
auto f7 = [=, &b] { return a + (b++); }; //OK

两个问题说明:

1.一个容易出错的细节是lambda表达式的延迟调用,lambda表达式按值捕获了所有外部变量。在捕获的一瞬间,a的值就已经被复制了。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。

1
2
3
4
5
6
7
8
9
int a = 0;
auto f = [=] { return a; };
a+=1;
cout << f() << endl; //输出0
int a = 0;
auto f = [&a] { return a; };
a+=1;
cout << f() <<endl; //输出1

2.虽然按值捕获的变量值均补复制一份存储在lambda表达式变量中, 修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。

那么如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。

需要注意:被mutable修饰的lambda表达式就算没有参数也要写明参数列表。

原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终均会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()const

1
2
3
int a = 0;
auto f1 = [=] { return a++; }; //Error
auto f2 = [=] () mutable { return a++; }; //OK

4.4 将lambda表达式用作返回值

一个没有指定任何捕获的lambda函数,可以显式转换成一个具有相同声明形式函数指针

1
2
int (*p)(void) = [] {return 1; };
cout << p();

定义在<functional>头文件中的std::function是多态的函数对象包装,类似函数指针。它可以绑定至任何可以被调用的对象(仿函数、成员函数指针、函数指针和lambda表达式),只要参数和返回类型符合包装的类型即可。返回一个double、接受两个整数参数的函数包装定义如下:

1
function<double(int, int)> myWrapper;

通过使用std::function,可从函数中返回lambda表达式,看一下定义:

1
2
3
function<int(void)> multiplyBy2Lambda(int x) {
return [=]()->int{return 2 * x; };
}

在这个例子中,lambda表达式的返回类型和空参数列表可以忽略,可改写为:

1
2
3
function<int(void)> multiplyBy2Lambda(int x) {
return[=] {return 2 * x; };
}

这个函数的主体部分创建了一个lambda表达式,这个lambda表达式通过值捕捉所在作用域的变量,并返回一个整数,这个返回的整数是传给multiplyBy2Lambda()的值的两倍。这个multiplyBy2Lambda()函数的返回值类型为 function<int(void)>,即一个不接受参数并返回一个整数的函数。函数主体中定义的lambda表达式正好匹配这个原型。变量x通过值捕捉,因此,在lambda表达式从函数返回之前,x值的一份副本绑定至lambda表达式中的x

可以通过以下方式调用上述函数:

1
2
function<int(void)> fn = mutiplyBy2Lambda(5);
cout << fn() << endl;

通过C++11的auto关键字可以简化这个调用:

1
2
auto fn = mutiplyBy2Lambda(5);
cout << fn() << endl;

输出为10。

mutiplyBy2Lambda()示例通过值捕捉了变量x:[=]。假设这个函数重写为通过引用捕捉变量:[&],如下所示。根据代码所示。根据代码后面的解释,下面这段代码不能正常工作:

1
2
3
function<int(void)> mutiplyBy2Lambda(int x) {
return[&] {return 2 * x; };
}

lambda表达式通过引用捕捉变量x。然而,lambda表达式会在程序后面执行,而不会在mutiplyBy2Lambda()函数的作用域中执行,在那里x的引用不再有效。

4.5 将lambda表达式用作参数

可以编写lambda表达式作为参数的函数。例如,可通过这种方式实现回调函数。下面的代码实现了一个testCallback()函数,这个函数接受一个整数vector和一个回调函数作为参数。这个实现迭代给定vector中的所有元素,并对每个元素调用回调函数,回调函数接受vector中每个元素作为int参数,并返回一个布尔值。如果回调函数返回false,那么停止迭代。

1
2
3
4
5
6
7
8
void testCallback(const vector<int>& vec, const function<bool(int)>& callback) {
for (auto i : vec) {
if (!callback(i))
break;
cout << i << " ";
}
cout << endl;
}

可以按照以下方式测试testCallback()函数。

1
2
3
4
5
6
vector<int> vec(10);
int index = 0;
generate(vec.begin(), vec.end(), [&index] {return ++index;});
for_each (vec.begin(), vec.end(), [](int i) {cout << i << " "; });
cout << endl;
testCallback(vec, [](int i){return i < 6; });

输出结果:

1
2
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5

五、移动语义move和右值引用

5.1 左值与右值

C++11中引入的一个非常重要的概念就是右值引用。理解右值引用是学习“移动语义”(move semantics)的基础。而要理解右值引用,就必须先区分左值与右值。

对左值和右值的一个最常见的误解是:等号左边的就是左值,等号右边的就是右值。

左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象

一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。下面给出一些例子来进行说明。

1
2
3
4
5
6
7
8
int a = 10;
int b = 20;
int *pFlag = &a;
vector<int> vctTemp;
vctTemp.push_back(1);
string str1 = "hello ";
string str2 = "world";
const int &m = 1;

请问,ab, a+b, a++, ++a, pFlag, *pFlag, vctTemp[0], 100, string("hello"), str1, str1+str2, m 分别是左值还是右值?

  • ab都是持久对象(可以对其取地址),是左值;
  • a+b是临时对象(不可以对其取地址),是右值;
  • a++是先取出持久对象a的一份拷贝,再使持久对象a的值加1,最后返回那份拷贝,而那份拷贝是临时对象(不可以对其取地址),故其是右值;
  • ++a则是使持久对象a的值加1,并返回那个持久对象a本身(可以对其取地址),故其是左值;
  • pFlag*pFlag都是持久对象(可以对其取地址),是左值;
  • vctTemp[0]调用了重载的[]操作符,而[]操作符返回的是一个int &,为持久对象(可以对其取地址),是左值;
  • 100string("hello")是临时对象(不可以对其取地址),是右值;
  • str1是持久对象(可以对其取地址),是左值;
  • str1+str2是调用了+操作符,而+操作符返回的是一个string(不可以对其取地址),故其为右值;
  • m是一个常量引用,引用到一个右值,但引用本身是一个持久对象(可以对其取地址),为左值。

5.2 左值引用

区分清楚了左值与右值,我们再来看看左值引用。左值引用根据其修饰符的不同,可以分为非常量左值引用常量左值引用

非常量左值引用只能绑定到非常量左值,不能绑定到常量左值、非常量右值和常量右值。

  • 如果允许绑定到常量左值和常量右值,则非常量左值引用可以用于修改常量左值和常量右值,这明显违反了其常量的含义。
  • 如果允许绑定到非常量右值,则会导致非常危险的情况出现,因为非常量右值是一个临时对象,非常量左值引用可能会使用一个已经被销毁了的临时对象。

常量左值引用可以绑定到所有类型的值,包括非常量左值、常量左值、非常量右值和常量右值。

可以看出,使用左值引用时,我们无法区分出绑定的是否是非常量右值的情况。那么,为什么要对非常量右值进行区分呢,区分出来了又有什么好处呢?这就牵涉到C++中一个著名的性能问题——拷贝临时对象。考虑下面的代码:

1
2
3
4
5
6
vector<int> GetAllScores() {
vector<int> vctTemp;
vctTemp.push_back(90);
vctTemp.push_back(95);
return vctTemp;
}

当使用vector<int> vctScore = GetAllScores()进行初始化时,实际上调用了三次构造函数。

  • 一次是vecTemp的构造,
  • 一次是return临时对象的构造,
  • 一次是vecScore的复制构造。

尽管有些编译器可以采用RVO(Return Value Optimization来进行优化,但优化工作只在某些特定条件下才能进行。

可以看到,上面很普通的一个函数调用,由于存在临时对象的拷贝,导致了额外的两次拷贝构造函数和析构函数的开销。当然,我们也可以修改函数的形式为void GetAllScores(vector<int> &vctScore),但这并不一定就是我们需要的形式。另外,考虑下面字符串的连接操作:

1
2
string s1("hello");
string s = s1 + "a" + "b" + "c" + "d" + "e";

在对s进行初始化时,会产生大量的临时对象,并涉及到大量字符串的拷贝操作,这显然会影响程序的效率和性能。

怎么解决这个问题呢?如果我们能确定某个值是一个非常量右值(或者是一个以后不会再使用的左值),则我们在进行临时对象的拷贝时,可以不用拷贝实际的数据,而只是“窃取”指向实际数据的指针(类似于STL中的auto_ptr,会转移所有权)。C++ 11中引入的右值引用正好可用于标识一个非常量右值。

C++ 11中用&表示左值引用,用&&表示右值引用,如:

1
int &&a = 10;

5.3 右值引用

右值引用根据其修饰符的不同,也可以分为非常量右值引用常量右值引用

非常量右值引用只能绑定到非常量右值,不能绑定到非常量左值、常量左值和常量右值。

  • 如果允许绑定到非常量左值,则可能会错误地窃取一个持久对象的数据,而这是非常危险的;
  • 如果允许绑定到常量左值和常量右值,则非常量右值引用可以用于修改常量左值和常量右值,这明显违反了其常量的含义。

常量右值引用可以绑定到非常量右值和常量右值,不能绑定到非常量左值和常量左值(理由同上)。

5.4 move 语句

有了右值引用的概念,我们就可以用它来实现下面的CMyString类。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class CMyString {
public:
// 构造函数
CMyString(const char *pszSrc = NULL) {
cout << "CMyString(const char *pszSrc = NULL)" << endl;
if (pszSrc == NULL) {
m_pData = new char[1];
*m_pData = '\0';
} else {
m_pData = new char[strlen(pszSrc)+1];
strcpy(m_pData, pszSrc);
}
}
// 拷贝构造函数
CMyString(const CMyString &s) {
cout << "CMyString(const CMyString &s)" << endl;
m_pData = new char[strlen(s.m_pData)+1];
strcpy(m_pData, s.m_pData);
}
// move构造函数 ---- 实质上就是·窃取·临时对象,注意参数的形式
CMyString(CMyString &&s) {
cout << "CMyString(CMyString &&s)" << endl;
m_pData = s.m_pData;
s.m_pData = NULL;
}
// 析构函数
~CMyString() {
cout << "~CMyString()" << endl;
delete [] m_pData;
m_pData = NULL;
}
// 拷贝赋值函数
CMyString &operator =(const CMyString &s) {
cout << "CMyString &operator =(const CMyString &s)" << endl;
if (this != &s) {
delete [] m_pData;
m_pData = new char[strlen(s.m_pData)+1];
strcpy(m_pData, s.m_pData);
}
return *this;
}
// move赋值函数
CMyString &operator =(CMyString &&s) {
cout << "CMyString &operator =(CMyString &&s)" << endl;
if (this != &s) {
delete [] m_pData;
m_pData = s.m_pData;
s.m_pData = NULL;
}
return *this;
}
private:
char *m_pData;
};

可以看到,上面我们添加了move版本的构造函数和赋值函数。那么,添加了move版本后,对类的自动生成规则有什么影响呢?唯一的影响就是,如果提供了move版本的构造函数,则不会生成默认的构造函数。另外,编译器永远不会自动生成move版本的构造函数和赋值函数,它们需要你手动显式地添加。

当添加了move版本的构造函数和赋值函数的重载形式后,某一个函数调用应当使用哪一个重载版本呢?下面是按照判决的优先级列出的3条规则:

  1. 常量值只能绑定到常量引用上,不能绑定到非常量引用上。

  2. 左值优先绑定到左值引用上,右值优先绑定到右值引用上。

  3. 非常量值优先绑定到非常量引用上。

当给构造函数或赋值函数传入一个非常量右值时,依据上面给出的判决规则,可以得出会调用move版本的构造函数或赋值函数。而在move版本的构造函数或赋值函数内部,都是直接“移动”了其内部数据的指针(因为它是非常量右值,是一个临时对象,移动了其内部数据的指针不会导致任何问题,它马上就要被销毁了,我们只是重复利用了其内存),这样就省去了拷贝数据的大量开销。

一个需要注意的地方是,拷贝构造函数可以通过直接调用*this = s来实现,但move构造函数却不能。这是因为在move构造函数中,s虽然是一个非常量右值引用,但其本身却是一个左值(是持久对象,可以对其取地址),因此调用*this = s时,会使用拷贝赋值函数而不是move赋值函数,而这已与move构造函数的语义不相符。要使语义正确,我们需要将左值绑定到非常量右值引用上,C++ 11提供了move函数来实现这种转换,因此我们可以修改为*this = move(s),这样move构造函数就会调用move赋值函数。