第18 章探讨 C++新标准 复习前面介绍过的 C++11 功能,移动语义和右值引用,为何需要移动语义
第18 章探讨 C++新标准 复习前面介绍过的 C++11 功能,移动语义和右值引用,为何需要移动语义
文章目录
18.2移动语义和右值引用
现在介绍本书前面未讨论的主题。C++11支持移动语义,这就提出了一些问题:什么是移动语义?
C++11如何支持它?为何需要移动语义?下面首先讨论第一个问题。
18.2.1 为何需要移动语义
先来看 C++11之前的复制过程。假设有如下代码:
vector<string> vstr;// build up a vector of 20,000 strings, each of 1000 characters
...
vector<string>vstr copy1(vstr);// make vstr copyl a copy of vstr
vector 和string 类都使用动态内存分配,因此它们必须定义使用某种new版本的复制构造函数。为初始化对象 vstr_copy1,复制构造函数vcctor将使用new给20000个sting对象分配内存,而每个sting对象又将调用 string的复制构造函数,该构造函数使用new为1000个字符分配内存。接下来,全部20000000个字符都将从 vstr控制的内存中复制到 vstrcopy1控制的内存中。这里的工作量很大,但只要妥当就行。但这确实妥当吗?有时候答案是否定的。例如,假设有一个函数,它返回一个 vector对象:
vectorsstring>allcaps(const vectorcstring>&vs)
vector<string> temp;
//code that stores an all-uppercase version of vs in temp
return temp;
接下来,假设以下面这种方式使用它:
vector<string>vstr;
// build up a vector of 20,000 strings, each of 1000 characters
vector<string>vstr copyl(vstr);// #1
vector<string>vstr copy2(allcaps(vstr));// #2
从表面上看,语句#1和#2类似,它们都使用一个现有的对象初始化一个 vcctor对象。如果深入探索这些代码,将发现allcaps()创建了对象temp,该对象管理着20000000个字符:vector和string的复制构造函数创建这 20000000个字符的副本,然后程序删除 allcaps()返回的临时对象(迟钝的编译器甚至可能将temp 复制给一个临时返回对象,删除temp,再删除临时返回对象)。这里的要点是,做了大量的无用功。考虑到临时对象被删除了,如果编译器将对数据的所有权直接转让给vstr_copy2,不是更好吗?也就是说不将20000000个字符复制到新地方,再删除原来的字符,而将字符留在原来的地方,并将vstr_copy2 与之相关联。这类似于在计算机中移动文件的情形:实际文件还留在原来的地方,而只修改记录。这种方法被称为移动语义(movesemantics)。有点悖论的是,移动语义实际上避免了移动原始数据,而只是修改了记录。
要实现移动语义,需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要。这就是右值引用发挥作用的地方。可定义两个构造函数。其中一个是常规复制构造函数,它使用const左值引用作为参数,这个引用关联到左值实参,如语句#1中的vstr;另一个是移动构造函数,它使用右值引用作为参数,该引用关联到右值实参,如语句#2中acaps(vstr)的返回值。复制构造函数可执行深复制,而移动构造函数只调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是 const。
18.2.2 一个移动示例
下面通过一个示例演示移动语义和右值引用的工作原理。程序清单18.2定义并使用了Useless 类,这个类动态分配内存,并包含常规复制构造函数和移动构造函数,其中移动构造函数使用了移动语义和右值引用。为演示流程,构造函数和析构函数都比较啰嗦,同时Useless 类还使用了一个静态变量来跟踪对象数量。另外,省略了一些重要的方法,如赋值运算符。
程序清单 18.2useless.cpp
// useless.cpp -- an otherwise useless class with move semantics
#include <iostream>
using namespace std;
// interface
class Useless
{
private:
int n; // number of elements
char * pc; // pointer to data
static int ct; // number of objects
void ShowObject() const;
public:
Useless();
explicit Useless(int k);
Useless(int k, char ch);
Useless(const Useless & f); // regular copy constructor
Useless(Useless && f); // move constructor
~Useless();
Useless operator+(const Useless & f)const;
// need operator=() in copy and move versions
void ShowData() const;
};
// implementation
int Useless::ct = 0;
Useless::Useless()
{
++ct;
n = 0;
pc = nullptr;
cout << "default constructor called; number of objects: " << ct << endl;
ShowObject();
}
Useless::Useless(int k) : n(k)
{
++ct;
cout << "int constructor called; number of objects: " << ct << endl;
pc = new char[n];
ShowObject();
}
Useless::Useless(int k, char ch) : n(k)
{
++ct;
cout << "int, char constructor called; number of objects: " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = ch;
ShowObject();
}
Useless::Useless(const Useless & f): n(f.n)
{
++ct;
cout << "copy const called; number of objects: " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = f.pc[i];
ShowObject();
}
Useless::Useless(Useless && f): n(f.n)
{
++ct;
cout << "move constructor called; number of objects: " << ct << endl;
pc = f.pc; // steal address
f.pc = nullptr; // give old object nothing in return
f.n = 0;
ShowObject();
}
Useless::~Useless()
{
cout << "destructor called; objects left: " << --ct << endl;
cout << "deleted object:\n";
ShowObject();
delete [] pc;
}
Useless Useless::operator+(const Useless & f)const
{
cout << "Entering operator+()\n";
Useless temp = Useless(n + f.n);
for (int i = 0; i < n; i++)
temp.pc[i] = pc[i];
for (int i = n; i < temp.n; i++)
temp.pc[i] = f.pc[i - n];
cout << "temp object:\n";
cout << "Leaving operator+()\n";
return temp;
}
void Useless::ShowObject() const
{
cout << "Number of elements: " << n;
cout << " Data address: " << (void *) pc << endl;
}
void Useless::ShowData() const
{
if (n == 0)
cout << "(object empty)";
else
for (int i = 0; i < n; i++)
cout << pc[i];
cout << endl;
}
// application
int main()
{
{
Useless one(10, 'x');
Useless two = one; // calls copy constructor
Useless three(20, 'o');
Useless four(one + three); // calls operator+(), move constructor
cout << "object one: ";
one.ShowData();
cout << "object two: ";
two.ShowData();
cout << "object three: ";
three.ShowData();
cout << "object four: ";
four.ShowData();
}
// cin.get();
}
其中最重要的是复制构造函数和移动构造函数的定义。首先来看复制构造函数(删除了输出语句):
Useless::Useless(constUseless &f):n(f.n)
{
++ct;
pc =new char[n];
for (int i=0;i < n; i++)
pc[i]= f.pe[i];
}
它执行深复制,是下面的语句将使用的构造函数:
Useless two one;//calls copy constructor
引用f将指向左值对象one。
接下来看移动构造函数,这里也删除了输出语句:
Useless::Useless(Useless && f):nf.n)
++ct;
//steal address
pc =f.pc;
f.pe =nullptr;//give old object nothing in return
f.n= 0;
它让 pc指向现有的数据,以获取这些数据的所有权。此时,pc和fpc指向相同的数据,调用析构函数时这将带来麻烦,因为程序不能对同一个地址调用dclete []两次。为避免这种问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行delete没有问题。这种夺取所有权的方式常被称为窃取(pilfering)。上述代码还将原始对象的元素数设置为零,这并非必不可少的,但让这个示例的输出更一致。注意,由于修改了f对象,这要求不能在参数声明中使用const。
在下面的语句中,将使用这个构造函数:
Useless four (one +three)://calls move constructor
表达式 one+three 调用Useless::operator+0,而右值引用f将关联到该方法返回的临时对象。下面是在Microsoft VisualC++2022中编译时,该程序的输出:
注意到没有调用移动构造函数,且只创建了4个对象。创建对象four时,该编译器没有调用任何构造函数:相反,它推断出对象four是 operator+()所做工作的受益人,因此将 operator+()创建的对象转到 fou的名下。一般而言,编译器完全可以进行优化,只要结果与未优化时相同。即使您省略该程序中的移动构造函数,并使用g++进行编译,结果也将相同。