响应式网站建设软文,浏览广告赚佣金的app,网上花店网页设计代码,南宁专业网站制作前十排名移动语义和右值引用
现在介绍本书前面未讨论的主题。C11 支持移动语义#xff0c;这就提出了一些问题#xff1a;为何需要移动语义#xff1f;什么是移动语义#xff1f;C11 如何支持它#xff1f;下面首先讨论第一个问题。
为何需要移动语义
先来看 C11 之前的复制过程…移动语义和右值引用
现在介绍本书前面未讨论的主题。C11 支持移动语义这就提出了一些问题为何需要移动语义什么是移动语义C11 如何支持它下面首先讨论第一个问题。
为何需要移动语义
先来看 C11 之前的复制过程。假设有如下代码
vectorstring vstr;
// build up a vector of 20,000 strings, each of 1000 characters
...
vectorstring vstr_copy1(vstr); // make vstr_copy1 a copy of vstrvector 和 string 类都使用动态内存分配因此它们必须定义使用某种 new 版本的复制构造函数。为初始化对象 vstr_copy1复制构造函数 vectorstring 将使用 new 给 20000 个 string 对象分配内存而每个string对象又将调用 string 的复制构造函数该构造函数使用 new 为 1000 个字符分配内存。接下来全部 20000000 个字符都将从 vstr 控制的内存中复制到 vstr_copy1 控制的内存中。这里的工作量很大但只要妥当就行。
但这确实妥当吗有时候答案是否定的。例如假设有一个函数它返回一个 vectorstring 对象
vectorstring allcaps(const vectorstring vs) {vectorstring temp;// code that stores an all-uppercase version of vs in tmepreturn temp;
}接下来假设以下面这种方式使用它
vectorstring vstr;
// build up a vector of 20,000 strings, each of 1000 characters
vectorstring vstr_copy1(vstr); // #1
vectorstring vstr_copy2(allcaps(vstr)); // #2从表面上看语句 #1 和 #2 类似它们都使用一个现有的对象初始化一个 vectorstring 对象。如果深入探索这些代码将发现 allcaps() 创建了对象 temp该对象管理着 20000000 个字符vector 和 string 的复制构造函数创建这 20000000 个字符的副本然后程序删除 allcaps() 返回的临时对象迟钝的编译器甚至可能将 temp 复制给一个临时返回对象删除 temp再删除临时返回对象。这里的要点是临时对象被复制后被删了。如果编译器将对数据的所有权直接从temp转让给 vstr_copy2不是更好吗也就是说不将 20000000 个字符复制到新地方再删除原来的字符而将字符留在原来的地方并将 vstr_copy2 与之相关联。这类似于在计算机中移动文件的情形实际文件还留在原来的地方而只修改记录。这种方法被称为移动语义move semantics。有点和字面意思看起来相悖的是移动语义实际上避免了移动原始数据而只是修改了记录。
要实现移动语义需要采取某种方式让编译器知道什么时候需要复制什么时候不需要。这就是右值引用发挥作用的地方。可定义两个构造函数。其中一个是常规复制构造函数它使用 const 左值引用作为参数这个引用关联到左值实参如 语句#1 中的 vstr另一个是移动构造函数它使用右值引用作为参数该引用关联到右值实参如语句 #2 中的 allcaps(vstr) 的返回值。复制构造函数可执行深复制而移动构造函数只调整记录。在将所有权转移给新对象的过程中移动构造函数可能修改其实参这意味着右值引用参数不应是 const。
一个移动示例
下面通过一个示例演示移动语义和右值引用的工作原理。下面的程序定义并使用了 Useless 类。这个类动态分配内存并包含常规复制构造函数和移动构造函数其中移动构造函数使用了移动语义和右值引用。为演示流程构造函数和析构函数都比较啰嗦同时 Useless 类还使用了一个静态变量来跟踪对象数量。另外省略了一些重要的方法如赋值运算符。
// useless.cpp -- an otherwise useless class with move semantics
#includeiostream
using namespace std;// interface
class Useless {
private:int n; // number of elementschar * pc; // pointer to datastatic int ct; // number of objectsvoid ShowObject() const;
public:Useless();explicit Useless(int k);Useless(int k, char ch);Useless(const Useless f); // regular copy constructorUseless(Useless f); // move constructor~Useless();Useless operator(const Useless f) const;// need operator() in copy and move versionsvoid ShowData() const;
};// implematation
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 constructor 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 addressf.pc nullptr; // give old object nothing in returnf.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(nf.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 element: 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; // calss copy constructorUseless three(20, o);Useless four (one three); // calls operator(), move contructorcout object one: ;one.ShowData();cout object two: ;two.ShowData();cout object three: ;three.ShowData();cout object four: ;four.ShowData();}
}其中最重要的是复制构造函数和移动构造函数的定义。首先来看复制构造函数删除了输出语句
Useless::Useless(const Useless f) : n(f.n) {ct;pc new char[n];for (int i 0; i n; i ) {pc[i] f.pc[i];}
}它执行深复制是下面的语句将使用的构造函数
Useless two one; // calls copy constructor引用 f 将指向左值对象 one。 接下来看移动构造函数这里也删除了输出语句
Useless::Useless(Useless f) : n(f.n) {ct;pc f.pc; // steal addressf.pc nullptr; // give old object nothing in returnsf.n 0;
}它让 pc 指向现有的数据以获取这些数据的所有权。此时pc 和 f.pc 指向相同的数据调用析构函数时这将带来麻烦因为程序不能对同一个地址调用 delete[] 两次。为避免这种问题该构造函数随后将原来的指针设置为空指针因为对空指针执行 delete[] 没有问题。这种夺取所有权的方式常被称为窃取pilfering)。上述代码还将原始对象的元素设置为零这并非必不可少的但让这个示例的输出更一致。注意由于修改了 f 对象这要求不能在参数声明中使用 const。
在下面的语句中将使用这个构造函数
Useless four (one three); // calls move constructor表达式 onethree 调用 Useless::operator()而右值引用 f 将关联到该方法返回的临时对象。
下面是在 MicroSoft Visual C 2010 中编译时该程序的输出
int, char constructor called; number of objects: 1
Number of element: 10 Data address: 0xabe2c0
copy constructor called; number of objects: 2
Number of element: 10 Data address: 0xabe2e0
int, char constructor called; number of objects: 3
Number of element: 20 Data address: 0xabe300
Entering operator()
int constructor called; number of objects: 4
Number of element: 30 Data address: 0xabe320
temp object:
Leaving operator()
move constructor called; number of objects: 5
Number of elements: 30 Data address: 0xabe320
destructor called; objects left: 4
deleted object:
Number of elements: 0 Data address: 00000000
object one: xxxxxxxxxx
object two: xxxxxxxxxx
object three: oooooooooooooooooooo
object four: xxxxxxxxxxoooooooooooooooooooo
destructor called; objects left: 3
deleted object:
Number of element: 30 Data address: 0xabe320
destructor called; objects left: 2
deleted object:
Number of element: 20 Data address: 0xabe300
destructor called; objects left: 1
deleted object:
Number of element: 10 Data address: 0xabe2e0
destructor called; objects left: 0
deleted object:
Number of element: 10 Data address: 0xabe2c0注意到对象 two 是对象 one 的副本他们显示的数据输出相同但显示的数据地址不同。另一方面在方法 Useless::operator() 中创建的对象的数据地址与对象 four 存储的数据地址相同其中对象 four 是由移动复制构造函数创建的。另外注意到创建对象 four 后为临时对象调用了析构函数。之所以知道这是临时对象是因为其元素和数据地址都是0.
如果使用编译器 g4.5.0 和 标记 -stdc11 编译该程序输出将不同这很有趣
int, char constructor called; number of objects: 1
Number of element: 10 Data address: 0xabe2c0
copy constructor called; number of objects: 2
Number of element: 10 Data address: 0xabe2e0
int, char constructor called; number of objects: 3
Number of element: 20 Data address: 0xabe300
Entering operator()
int constructor called; number of objects: 4
Number of element: 30 Data address: 0xabe320
temp object:
Leaving operator()
object one: xxxxxxxxxx
object two: xxxxxxxxxx
object three: oooooooooooooooooooo
object four: xxxxxxxxxxoooooooooooooooooooo
destructor called; objects left: 3
deleted object:
Number of element: 30 Data address: 0xabe320
destructor called; objects left: 2
deleted object:
Number of element: 20 Data address: 0xabe300
destructor called; objects left: 1
deleted object:
Number of element: 10 Data address: 0xabe2e0
destructor called; objects left: 0
deleted object:
Number of element: 10 Data address: 0xabe2c0注意到没有调用移动构造函数且只创建了 4 个对象。创建对象 four 时该编译器没有调用任何构造函数相反它推断出对象 four 是 operator() 所做的工作的受益人因此将 operator()创建的对象转到 four 的名下。一般而言编译器完全可以进行优化只要结果与未优化时相同。即使您省略该程序中的移动构造函数并使用 g 进行编译结果也将相同。
移动构造函数解析
虽然使用右值引用可支持移动语义但这并不会神奇地发生。要让移动语义发生需要两个步骤。首先右值引用让编译器知道何时可使用移动语义
Useless two one; // matches Useless::Useless(const Useless )
Useless four ( one three); // matches Useless::Useless(Useless )对象 one 是左值与左值引用匹配而表达式 onethree 是右值与右值引用匹配。因此右值引用让编译器使用移动构造函数来初始化对象 four。实现移动语义的第二步是编写移动构造函数使其提供所需的行为。
总之通过提供一个使用左值引用的构造函数和一个使用右值引用的构造函数将初始化分成了两组。使用左值对象初始化对象时将使用复制构造函数而使用右值对象初始化对象时将使用移动构造函数。程序员可根据需要赋予这些构造函数不同的行为。
这就带来了一个问题在引入右值引用前情况是什么样的呢如果没有移动构造函数且编译器未能通过优化消除对复制构造函数的需求结果将如何呢在 C98 中下面的语句将调用复制构造函数
Useless four (one three);但左值引用不能指向右值。结果将如何呢第 8 章介绍过如果实参为右值const 引用形参将指向一个临时变量
int twice(const int rx) {return 2 * rx;
}
...
int main() {int m 6;// below, rx refers to mint n twice(m);// below, rx refers to a temporary variable initialized to 21int k twice(21);
}就 Useless 而言形参 f 将被初始化一个临时对象而该临时对象被初始化为 operator() 返回的值。下面是使用老式编译器进行编译时之前的程序删除了移动构造函数的部分输出
Entering operator()
int constructor called; number of objects: 4
Number of element: 30 Data address: 0x1785320
temp object:
Leaving operator()
copy constructor called; number of objects: 5
Number of element: 30 Data address: 0x1785340
destructor called; objects left: 4
deleted object:
Number of element: 30 Data address: 0x1785320
copy constructor called; number of objects: 5
Number of element: 30 Data address: 0x1785320
destructor called; objects left: 4
deleted object:
Number of element: 30 Data address: 0x1785340首先在方法 Useless::operator()内调用构造函数创建了 temp并在 0x1785320 给它分配了存储 30 个元素的空间。然后调用复制构造函数创建了一个临时复制信息其地址为 0x1785340f 指向该副本。接下来删除了地址为 0x1785320 的对象 temp。然后新建了对象 four它使用了 0x1785320 处刚释放的内存。接下来删除了 0x1785340 处的临时参数对象。这表明总共创建了三个对象但其中的两个被删除。这些就是移动语义旨在消除的额外工作。
正如 g 示例表明的机智的编译器可能自动消除额外的复制工作但通过使用右值引用程序员可以指出何时该使用移动语义。
赋值
适用于构造函数的移动语义考虑也适用于赋值运算符。例如下面演示了如何给 Useless 类编写复制赋值运算符和移动赋值运算符
Useless Useless::operator(const Useless f) { // copy assignmentif (this if) return *this;delete [] pc;n f.n;pc new char[n];for ( int i 0; i n; i ) {pc[i] f.pc[i];}return *this;
}Useless Useless::operator(Useless f) { // move assignmentif (this f) {return *this;}delete [] pc;n f.n;pc f.pc;f.n 0;f.pc nullptr;return *this;
}上述赋值运算符采用了第12章介绍的常规模式而移动赋值运算符删除目标对象中的原始数据并将源对象的所有权转让给目标。不能让多个指标指向相同的数据这很重要因此上述代码将源对象中的指针设置为空指针。
与移动构造函数一样移动赋值运算符的参数也不能是 const 引用因为这个方法修改了源对象。
强制移动
移动构造函数和移动赋值运算符使用右值。如果要让它们使用左值该如何办呢例如程序可能分析一个包含候选对象的数组选择其中一个对象供以后使用并丢弃数组。如果可以使用移动构造函数或移动赋值运算符来保留选定的对象那该多好啊。然而假设您试图像下面这样做
Useless choices[10];
Useless best;
int pick;
... // select one object, set pick to index
best choices[pick];由于 choices[pick] 是左值因此上述赋值语句将使用复制赋值运算符而不是移动赋值运算符。但如果能让 choices[pick] 看起来像右值变将使用移动赋值运算符。为此可使用运算符 static_cast 将对象的类型强制转换为 Useless 但 C 提供了一种更简单的方式——使用头文件 utility 中声明的函数 std::move()。下面的程序演示了这种技术它在 Useless 类中添加了啰嗦的赋值运算符并让以前啰嗦的构造函数和析构函数保持沉默。
// stdmove.cpp -- using std::move()
#includeiostream
#includeutility// interface
class Useless {
private:int n; // number of elementschar * pc; // pointer to datastatic int ct; // number of objectsvoid ShowObject() const;
public:Useless();explicit Useless(int k);Useless(int k, char ch);Useless(const Useless f); // regular copy constructorUseless(Useless f); // move constructor~Useless();Useless operator(const Useless f) const;Useless operator(const Useless f); // copy assignmentUseless operator(Useless f); // move assignmentvoid ShowData() const;
};// implementation
int Useless::ct 0;Useless::Useless() {ct;n 0;pc nullptr;
}Useless::Useless(int k) : n(k) {ct;pc new char[n];
}Useless::Useless(int k, char ch) : n(k) {ct;pc new char[n];for (int i 0; i n; i ){pc[i] ch;}
}Useless::Useless(const Useless f): n(f.n) {ct;pc new char[n];for(int i 0; i n; i ){pc[i] f.pc[i];}
}Useless::Useless(Useless f) : n(f.n) {ct;pc f.pc; // steal addressf.pc nullptr; // give old object nothing in returnf.n 0;
}Useless::~Useless() {delete [] pc;
}Useless Useless::operator(const Useless f) { // copy assignmentstd::cout copy assignment operator called:\n;if (this f){return *this;}delete[] pc;n f.n;pc new char[f.n];for (int i 0; i n; i){pc[i] f.pc[i];}return *this;
}Useless Useless::operator(Useless f) { // move assignmentstd::cout move assignment operator called:\n;if (this f) {return *this;}delete [] pc;n f.n;pc f.pc;f.n 0;f.pc nullptr;return *this;
}Useless Useless::operator(const Useless f) const {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];}return temp;
}void Useless::ShowObject() const {std::cout Number of elements: n;std::cout Data address: (void *) pc std::endl;
}void Useless::ShowData() const {if (n 0){std::cout (object empty);}else{for (int i 0; i n; i){std::cout pc[i];}}std::cout std::endl;
}// application
int main(){using std::cout;{Useless one(10, x);Useless two one one; // calls move contructorcout object one: ;one.ShowData();cout object two: ;two.ShowData();Useless three, four;cout three one\n;three one;cout now object three ;three.ShowData();cout and object one ;one.ShowData();cout four one two\n;four one two; // automatic move assignmentcout now object four ;four.ShowData();cout four move(one)\n;four std::move(one); // forced move assignmentcout now object four ;four.ShowData();cout and object one ;one.ShowData();}return 0;
}该程序的输出如下
object one: xxxxxxxxxx
object two: xxxxxxxxxxxxxxxxxxxx
three one
copy assignment operator called:
now object three xxxxxxxxxx
and object one xxxxxxxxxx
four one two
move assignment operator called:
now object four xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
four move(one)
move assignment operator called:
now object four xxxxxxxxxx
and object one (object empty)正如您看到的将 one 赋给 three 调用了复制赋值运算符但将 move(one) 赋给 four 调用的是移动赋值运算符。
需要知道的是函数 std::move() 并非一定会导致移动操作。例如假设 Chunk 是一个包含私有数据的类而您编写了如下代码
Chunk one;
...
Chunk two;
two std::move(one); // move semantics?表达式 std::move(one) 是右值因此上述赋值语句将调用 Chunk 的移动赋值运算符——如果定义了这样的运算符。但如果 Chunk 没有定义移动赋值运算符编译器将使用复制赋值运算符。如果也没有定义复制赋值运算符将根本不允许上述赋值。
对大多数程序员来说右值引用带来的主要好处并非是让他们能够编写使用右值引用的代码而是能够使用利用右值引用实现移动语义的库代码。例如STL 类现在都有复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符。