C++03 示例(在移动语义之前)
假设我们有以下程序:


#include <string>
#include <vector>std::vector<std::string> createAndInsert()
{std::vector<std::string> coll; // create vector of stringscoll.reserve(3); // reserve memory for 3 elementsstd::string s = "data";// create string objectcoll.push_back(s); // insert string objectcoll.push_back(s+s); // insert temporary stringcoll.push_back(s);  // insert stringreturn coll; // return vector of strings
}int main()
{std::vector<std::string> v;//create empty vector strings...v = createAndInsert(); //assign returned vector of strings...
}

当我们使用不支持移动语义的 C++ 编译器编译该程序时,让我们看看程序的各个步骤(检查堆栈和堆)。

  1. 首先,在main()中,我们创建空vector v:它作为一个对象放置在堆栈上,其中元素的数量为 0,并且没有为元素分配内存。
  2. 我们调用v = createAndInsert();我们在堆栈上创建另一个空vector coll 并为堆上的三个元素保留内存:
    std::vector<std::string> coll;
    coll.reserve(3);
    分配的内存没有初始化,因为元素个数还是0
    然后,我们创建一个用"data"初始化的字符串:
    std::string s = "data";
    字符串类似于带有 char 元素的vector。 本质上,我们在堆栈上创建一个对象,其中包含一个字符数成员(值为 4)和一个指向字符内存的指针。
    在这条语句之后,程序具有以下状态:我们在堆栈上有三个对象:v、coll 和 s。 其中两个,coll 和 s,在堆上分配了内存:

C++ 标准库中的所有容器都具有值语义,这意味着它们会创建传递给它们的值的副本。 结果,我们得到了vector中的第一个元素,它是传递的值/对象 s 的完整(深拷贝)副本:

到目前为止,我们在这个程序中没有什么可以优化的。 当前状态是我们有两个vector v 和 coll,以及两个字符串 s 和它的副本,这是 coll 中的第一个元素。 它们都应该是单独的对象,具有自己的值内存,因为修改其中一个不应影响任何其他对象。

现在让我们看看下一条语句,它创建一个新的临时字符串并再次将其插入vector中:
coll.push_back(s+s);
该语句分三步执行:
1.我们创建临时字符串 s+s:

2.我们将这个临时字符串插入到vector coll 中。 与往常一样,容器会创建传递值的副本,这意味着我们会创建临时字符串的深拷贝,包括为值分配内存:

3.在语句结束时,临时字符串 s+s 被销毁,因为我们不再需要它:

在这里,我们第一次生成了性能不佳的代码:我们创建了一个临时字符串的副本,然后立即销毁副本的源,这意味着我们不必要地分配和释放了我们本可以移动的内存 从源到副本。
4.在下一条语句中,我们再次将 s 插入到 coll 中:
coll.push_back(s);
同样, coll 复制 s:

这也是需要改进的地方:因为不再需要 s 的值,所以一些优化可以使用 s 的内存作为vector中新元素的内存。
5.在 createAndInsert() 结束时,我们来到 return 语句:
return coll;

在这里,程序的行为变得有点复杂。 我们按值返回(返回类型不是引用),应该是return语句中的值的副本,coll。 创建 coll 的副本意味着我们必须创建整个vector及其所有元素的深拷贝的副本。 因此,我们必须为vector中的元素数组分配堆内存,并为每个字符串分配的值分配堆内存以保存其值。 在这里,我们必须分配内存 4 次。

但是,由于同时 coll 被销毁,因为我们离开了声明它的范围,所以允许编译器执行命名返回值优化 (NRVO)。 这意味着编译器可以生成代码,以便 coll 仅用作返回值。

即使这会改变程序的功能行为,也允许这种优化。 如果我们在vector或字符串的复制构造函数中有一个 print 语句,我们会看到程序不再有 print 语句的输出。 这意味着这种优化改变了程序的功能行为。 但是,这没关系,因为我们在 C++ 标准中明确允许这种优化,即使它有副作用。 没有人应该期望复制在这里完成,就像没有人应该期望它不是一样。 是否执行命名返回值优化完全取决于编译器。

让我们假设我们有命名的返回值优化。 在这种情况下,在 return 语句的末尾, coll 现在成为返回值,并调用 s 的析构函数,释放声明时分配的内存:

最后,我们来给 v 赋值:
v = createAndInsert();
在这里,我们确实得到了可以改进的行为:通常的赋值运算符的目标是赋予 v 与分配的源值相同的值。 通常,不应修改任何分配的值,并且应独立于分配该值的对象。 因此,赋值运算符将创建整个返回值的深拷贝的副本:

然而,在那之后,我们不再需要临时返回值并销毁它:

同样,我们创建一个临时对象的副本并在之后立即销毁副本的源,这意味着我们再次不必要地分配和释放内存。 这次它适用于四个分配,一个用于vector,一个用于每个字符串元素。
对于这个程序在main()中赋值后的状态,我们分配了十次内存,释放了六次。 不必要的内存分配是由以下原因引起的:
1.将vector作为输出参数传递:
createAndInsert(v);//let the function fill vector v
2.使用swap():
createAndInsert().swap(v);
但是,生成的代码看起来更丑陋(除非您在复杂代码中看到一些美感),并且在插入临时对象时没有真正的解决方法。
从 C++11 开始,我们有了另一个选择:编译并运行支持移动语义的程序。

自 C++11 以来的示例(使用移动语义)
现在让我们使用支持移动语义的现代 C++ 编译器(C++11 或更高版本)重新编译程序:


#include <string>
#include <vector>std::vector<std::string> createAndInsert()
{std::vector<std::string> coll; // create vector of stringscoll.reserve(3); // reserve memory for 3 elementsstd::string s = "data";// create string objectcoll.push_back(s); // insert string objectcoll.push_back(s+s); // insert temporary stringcoll.push_back(std::move(s));  // insert stringreturn coll; // return vector of strings
}int main()
{std::vector<std::string> v;//create empty vector strings...v = createAndInsert(); //assign returned vector of strings...
}

不过有一个小的修改:当我们将最后一个元素插入 coll 时,我们添加了一个 std::move() 调用。 当我们谈到这个声明时,我们将讨论这个变化。 其他一切都和以前一样。
1.首先,在 main() 中,我们创建了一个空vector v,它被放置在具有 0 个元素的堆栈上:
std::vector<std::string> v;
2.然后调用
v = createAndInsert();
我们在堆栈上创建另一个空vector coll 并为堆上的三个元素保留未初始化的内存:
std::vector<std::string> coll;
coll.reserve(3)
3.然后,我们创建用“data”初始化的字符串 s 并再次将其插入到 coll 中:
std::string s = "data"
coll.push_back(s)
到目前为止,没有什么可优化的,我们得到了与 C++03 相同的状态:

我们有两个vector v 和 coll,以及两个字符串 s 和它的副本,它是 coll 中的第一个元素。 它们都应该是单独的对象,具有自己的值内存,因为修改其中一个不应影响任何其他对象。

这就是事情发生变化的地方。 首先,让我们看一下创建一个新的临时字符串并将其插入vector中的语句:
coll.push_back(s+s);
同样,此语句分三个步骤执行:

  1. 我们创建临时字符串 s+s:

  2. 我们将这个临时字符串插入到vector coll 中。 然而,这里发生了一些不同的事情:我们从 s+s 中窃取值的内存并将其移动到 coll 的新元素中。

这是可能的,因为从 C++11 开始,我们可以实现特殊行为来获取不再需要的值。 编译器可以发出信号,因为它知道在执行 push_back() 调用之后,临时对象 s+s 将被销毁。 因此,我们调用为调用者不再需要该值的情况提供的不同的 push_back() 实现。 正如我们所看到的,效果是在我们不再需要值的地方复制字符串的优化实现:我们不是创建单独的深拷贝副本,而是复制大小和指向内存的指针。 然而,这种浅拷贝是不够的; 我们还通过将大小设置为 0 并将 nullptr 分配为新值来修改临时对象 s+s。 本质上, s+s 被修改,使其获得空字符串的状态。 重要的一点是它不再拥有它的内存。 这很重要,因为我们在此声明中还有第三步。
3. 在语句结束时,临时字符串 s+s 被销毁,因为我们不再需要它。 但是,因为临时字符串不再是初始内存的所有者,所以析构函数不会释放这块内存。

本质上,我们优化了复制,以便我们将 s+s 值的内存所有权转移到它在向量中的副本。
这一切都是通过使用可以发出对象即将死亡的信号的编译器自动完成的,因此我们可以使用新的实现来复制从源中窃取值的字符串值。 它是一种语义移动,通过技术上将值的内存从源字符串移动到其副本来实现。
下一条语句是我们为 C++11 版本修改的语句。 同样,我们将 s 插入到 coll 中,但是通过为我们插入的字符串 s 调用 std::move() 来更改语句:
coll.push_back(std::move(s))
如果没有 std::move(),与第一次调用 push_back() 时会发生相同的情况:向量将创建传递的字符串 s 的深层副本。 然而,在这个调用中,我们用 std::move() 标记了 s,这在语义上意味着“我在这里不再需要这个值”。 结果,我们又调用了 push_back() 的另一个实现,它在我们传递临时对象 s+s 时使用。 第三个元素通过将值的内存所有权从 s 移动到其副本来窃取值:

请注意以下两个非常重要的关于移动语义的理解:

  1. std::move(s) 仅将 s 标记为在此上下文中可移动。 它不会移动任何东西。 它只是说,“我在这里不再需要这个值。” 它允许调用的实现通过在复制值时执行一些优化来从这个标记中受益,例如窃取内存。 值是否被移动是调用者不知道的。
  2. 但是,窃取值的优化必须确保源对象仍处于有效状态。 移出的对象既不会部分也不会完全破坏。 C++ 标准库对其类型进行了如下表述:在对标有 std::move() 的对象调用操作后,该对象处于有效但未指定的状态。
    也就是说,调用coll.push_back(std::move(s));后保证 s 仍然是一个有效的字符串。 只要它对您不需要知道该值的任何字符串有效,您就可以做任何您想做的事情。 这就像使用一个字符串参数,您不知道传递了哪个值。
    请注意,也不能保证字符串具有其旧值或为空。 它的值取决于(库)函数的实现者。 一般来说,实现者可以随心所欲地使用标有 std::move() 的对象,只要他们让对象处于有效状态。 这种保证有充分的理由,稍后将讨论。
    同样,在 createAndInsert() 结束时,我们来到 return 语句:
    return coll;
    是否生成具有命名返回值优化的代码仍然取决于编译器,这意味着 coll 只是成为返回值。 但是,如果不使用这种优化,return 语句仍然很方便,因为我们又遇到了一种情况,即我们从一个即将死亡的源创建一个对象。 即如果不使用命名返回值优化,将使用移动语义,这意味着返回值从coll中窃取值。 在最坏的情况下,我们必须将成员的大小、容量和指向内存的指针(总共,通常为 12 或 24 个字节)从源复制到返回值,并为源中的这些成员分配新值。

所以,最后,我们来给 v 赋值:
v = createAndInsert();
同样,我们现在可以从移动语义中受益,因为我们已经看到了一种情况:我们必须从即将死亡的临时返回值中复制(在这里,分配)一个值。
现在,移动语义允许我们为只从源vector中窃取值的vector提供赋值运算符的不同实现:

同样,临时对象没有(部分)被破坏。 它进入有效状态,但我们不知道它的值。

最后,我们处于与使用移动语义之前相同的状态,但一些重要的事情发生了变化:
我们保存了六次内存分配和释放。所有不必要的内存分配不再发生:
在第二种情况下,优化是在我们的帮助下完成的。通过添加 std::move(),我们不得不说我们不再需要 s 的值。所有其他优化的发生是因为编译器知道一个对象即将死亡,这意味着它可以调用使用移动语义的优化实现。
这意味着返回字符串vector并将其分配给现有vector不再是性能问题。我们可以像整数类型一样天真地使用字符串vector并获得更好的性能。
在实践中,使用移动语义重新编译代码可以将速度提高 10% 到 40%。

实现移动语义
让我们使用前面的例子来看看移动语义是在哪里以及如何实现的。 在实现移动语义之前,类 std::vector<> 只有一种 push_back() 实现(这里简化了 vector 的声明):


template<typename T>
class vector
{public:...// insert a copy of elem:void push_back (const T& elem);...
};

只有一种方法可以将参数传递给 push_back() 并将其绑定到 const 引用。 push_back() 的实现方式是vector创建传递参数的内部副本而不修改它。
从 C++11 开始,我们有第二个 push_back() 重载:


template<typename T>
class vector
{public:...// insert a copy of elem:void push_back (const T& elem);// insert elem when the value of elem is no longer needed:void push_back (T&& elem);...
};

第二个 push_back() 使用为移动语义引入的新语法。 我们用两个 & 和不带 const 声明参数。 这样的参数称为右值引用。 只有一个 & 的“普通引用”现在称为左值引用。 也就是说,在这两个调用中,我们通过引用传递要插入的值。 但是,区别如下:
使用 push_back(const T&),我们保证不会修改传递的值。 当调用者仍然需要传递的值时调用此函数。
使用 push_back(T&&),实现可以修改传递的参数(因此它不是 const)以“窃取”值。 语义仍然是新元素接收传递参数的值,但我们可以使用优化的实现将值移动到vector中。 当调用者不再需要传递的值时调用此函数。 实现必须确保传递的参数仍处于有效状态。 但是,该值可能会改变。 因此,在调用 this 之后,只要调用者不对它的值做任何假设,调用者仍然可以使用传递的参数。
但是,vector不知道如何复制或移动元素。 在确保vector有足够的内存存储新元素之后,vector将工作委托给元素的类型。 在这种情况下,元素是字符串。 那么,让我们看看如果我们复制或移动传递的字符串会发生什么。

使用复制构造函数

传统复制语义的 push_back(const T&) 调用字符串类的复制构造函数,它初始化vector中的新元素。 让我们看看这是如何实现的。 一个非常简单的字符串类实现的复制构造函数如下所示:


class string
{private:int len;// current number of characterschar* data;// dynamic array of characters
public:// copy constructor: create a full copy of s:string (const string& s):len{s.len} {// copy number of charactersif (len > 0) {// if not emptydata = new char[len+1];// - allocate new memorymemcpy(data, s.data, len+1); // - and copy the characters}}
...
};

假设我们为具有值“data”的字符串调用此复制构造函数:
std::string a = "data";
std::string b = a;// create b as a copy of a
初始化字符串 a 后如下:

上面的复制构造函数将复制成员 len 的字符数,但为数据指针的值分配新内存,并将源 a(作为 s 传递)中的所有字符复制到新字符串:

使用移动构造函数

新移动语义的 push_back(T&&) 调用相应的新构造函数,即移动构造函数。 这是从现有字符串创建新字符串的构造函数,其中不再需要该值。 与移动语义一样,构造函数声明为使用非常量右值引用 (&&) 作为其参数:


class string
{private:int len;// current number of characterschar* data;// dynamic array of characters
public:...// move constructor: initialize the new string from s (stealing the value):string (string&& s):len{s.len}, data{s.data} { // copy number of characters and pointer to memorys.data = nullptr;// release the memory for the source values.len = 0;// and adjust number of characters accordingly}...
};

鉴于上述复制构造函数的情况:

我们可以为字符串调用这个构造函数,如下所示:
std::string c = std::move(b);//init c with the value of b (no longer needing its value here)
移动构造函数首先复制成员 len 和 data,这意味着新字符串获得 b 值的所有权(作为 s 传递)。

然而,这还不够,因为 b 的析构函数会释放内存。 因此,我们还修改了源字符串以失去其对内存的所有权,并使其进入表示空字符串的一致状态:

效果是 c 现在具有 b 的先前值,并且 b 是空字符串。 同样,请注意,唯一的保证是 b 随后处于有效但未指定的状态。 根据在 C++ 库中实现移动构造函数的方式,它可能不是空的(但通常是因为这是提高性能的最简单和最好的方法)。

Copying as a Fallback
我们看到,通过使用临时对象或使用 std::move() 标记对象,我们可以启用移动语义。 提供特殊实现的函数(通过采用非 const 右值引用)可以通过从源中“窃取”值来优化值的复制。 但是,如果没有针对移动语义的函数的优化版本,则使用通常的复制作为后备。
例如,假设像 vector 这样的容器类缺少 push_back() 的第二个重载:


template<typename T>
class MyVector
{public:...void push_back (const T& elem); // insert a copy of elem...// no other push_back() declared
};

我们仍然可以传递一个临时对象或一个用 std::move() 标记的对象:


MyVector<std::string> coll;
std::string s{"data"};
...
coll.push_back(std::move(s)); // OK, uses copy semantics

规则是,对于临时对象或标有 std::move() 的对象,如果可用,则首选将参数声明为右值引用的函数。 但是,如果不存在这样的函数,则使用通常的复制语义。 这样,我们确保调用者不必知道是否存在优化。 优化可能不存在,因为:

  1. 该函数/类是在支持移动语义之前实现的,或者没有考虑移动语义支持
  2. 没有什么可优化的(只有数字成员的类就是一个例子)
    对于通用代码,如果我们不再需要它的值,我们总是可以用 std::move() 标记一个对象,这一点很重要。 即使没有移动语义支持,相应的代码也会编译。

std::vector<int> coll;
int x{42};
...
coll.push_back(std::move(x));
// OK, but copies x (std::move() has no effect)

为 const 对象移动语义
最后,请注意不能移动使用 const 声明的对象,因为任何优化实现都要求可以修改传递的参数。 如果我们不允许修改它,我们就不能窃取它。


template<typename T>
class vector
{public:...// insert a copy of elem:void push_back (const T& elem);// insert elem when the value of elem is no longer needed:void push_back (T&& elem);...
};

调用 const 对象的唯一有效函数是带有 const& 参数的 push_back() 的第一个重载:


std::vector<std::string> coll;
const std::string s{"data"};
...
coll.push_back(std::move(s)); // OK, calls push_back(const std::string&)

这意味着 const 对象的 std::move() 基本上没有效果。
原则上,我们可以通过声明一个采用 const 右值引用的函数来为这种情况提供一个特殊的重载。 但是,这没有语义意义。 同样, const 左值引用重载用作处理这种情况的后备。

常量返回值
const 禁用移动语义这一事实也会对声明返回类型产生影响。 不能移动 const 返回值。
因此,从 C++11 开始,使用 const 按值返回不再是一种好的风格(正如过去一些风格指南所推荐的那样)。 例如:


const std::string getValue();
std::vector<std::string> coll;
...
coll.push_back(getValue()); // copies (because the return value is const)

按值返回时,不要将返回值整体声明为const。 仅使用 const 来声明返回类型的一部分(例如返回的引用或指针所指的对象):


const std::string getValue(); // BAD: disables move semantics for return values
const std::string& getRef();
// OK
const std::string* getPtr();
// OK

总结

  1. 移动语义允许我们优化对象的复制,我们不再需要该值。 它可以隐式使用(用于未命名的临时对象或本地返回值)或显式使用(使用 std::move())。
  2. std::move() 意味着我在这里不再需要这个值。 它将对象标记为可移动的。 标有 std::move() 的对象不会(部分)销毁(仍将调用析构函数)。
  3. 通过声明一个具有非常量右值引用的函数(例如 std::string&&),您定义了一个接口,调用者在该接口中语义上声明它不再需要传递的值。 函数的实现者可以使用此信息通过“窃取”值或对传递的参数进行任何其他修改来优化其任务。 通常,实现者还必须确保传递的参数在调用后处于有效状态。
  4. C++ 标准库的Moved-from对象仍然是有效对象,但您不再知道它们的值。
  5. 复制语义用作移动语义的后备(如果支持复制语义)。 如果没有采用右值引用的实现,则使用采用普通 const 左值引用的任何实现(例如 const std::string&)。 即使对象被显式标记为 std::move(),也会使用此作为后备。
  6. 为 const 对象调用 std::move() 通常无效。
  7. 如果按值返回(不是按引用),不要将返回值作为一个整体声明为 const。

移动语义的核心特征
Rvalue References in Detail
右值引用用两个 & 符号声明。 就像普通的引用一样,它用一个 & 号声明,现在称为左值引用,右值引用引用一个必须作为初始值传递的现有对象。 然而,根据它们的语义,右值引用只能引用一个没有名字的临时对象或一个用 std::move() 标记的对象:


std::string returnStringByValue();// forward declaration
...
std::string s{"hello"};
...
std::string&& r1{s};// ERROR
std::string&& r2{std::move(s)};// OK
std::string&& r3{returnStringByValue()}; // OK, extends lifetime of return value

用于初始化引用的语法无关紧要。 您可以使用等号、大括号或圆括号:


std::string s{"hello"};
...
std::string&& r1 = std::move(s); // OK, rvalue reference to s
std::string&& r2{std::move(s)};  // OK, rvalue reference to s
std::string&& r3(std::move(s));  // OK, rvalue reference to s

所有这些引用都具有“我们可以窃取/修改我们引用的对象,只要对象的状态保持有效状态”的语义。 从技术上讲,编译器不会检查这些语义,因此我们可以修改右值引用,就像我们可以修改该类型的任何非 const 对象一样。 我们也可能决定不修改该值。 也就是说,如果您有一个对象的右值引用,则该对象可能会收到不同的值(可能是也可能不是默认构造对象的值),或者它可能会保留其值。
正如我们所见,移动语义允许我们使用不再需要该值的源值进行优化。 如果编译器自动检测到使用了处于生命周期末尾的对象的值,它们将自动切换到移动语义。 在以下情况下会出现这种情况:

  1. 我们传递一个临时对象的值,该对象将在语句之后自动销毁。
  2. 我们传递一个用 std::move() 标记的非常量对象。

右值引用作为参数
当我们将参数声明为右值引用时,它完全具有上面介绍的行为和语义:

  1. 该参数只能绑定到没有名称的临时对象或用 std::move() 标记的对象。
  2. 根据右值引用的语义:
    – 调用者声称它不再对该值感兴趣。 因此,您可以修改参数引用的对象。
    – 然而,调用者可能仍然对使用该对象感兴趣。 因此,任何修改都应使引用的对象保持有效状态。
    例如

void foo(std::string&& rv); // takes only objects where we no longer need the value
...
std::string s{"hello"};
...
foo(s);// ERROR
foo(std::move(s));// OK, value of s might change
foo(returnStringByValue()); // OK

您可以在使用 std::move() 传递命名对象后使用它,但通常不应该这样做。 推荐的编程风格是在 std::move() 之后不再使用对象:


void foo(std::string&& rv); // takes only objects where we no longer need the value
...
std::string s{"hello"};
...
foo(std::move(s));          // OK, value of s might change
std::cout << s << ' \n ' ;  // OOPS, you don’t know which value is printed
foo(std::move(s));          // OOPS, you don’t know which value is passed
s = "hello again";           // OK, but rarely done
foo(std::move(s));          // OK, value of s might change

std::move()
如果你有一个对象,当你使用它时,它的生命周期没有结束,你可以用 std::move() 标记它,表示“我这里不再需要这个值”。 std::move() 不move; 它只在使用表达式的上下文中设置一个临时标记:


void foo1(const std::string& lr); // binds to the passed object without modifying it
void foo1(std::string&& rv);      // binds to the passed object and might steal/modify the value
...
std::string s{"hello"};
...
foo1(s);                          // calls the first foo1(), s keeps its value
foo1(std::move(s));               // calls the second foo1(), s might lose its value

标有 std::move() 的对象仍然可以传递给采用普通 const 左值引用的函数:


void foo2(const std::string& lr); // binds to the passed object without modifying it
...                               // no other overload of foo2()
std::string s{"hello"};
...
foo2(s);                          // calls foo2(), s keeps its value
foo2(std::move(s));               // also calls foo2(), s keeps its value

请注意,标有 std::move() 的对象不能传递给非常量左值引用:


void foo3(std::string&);    // modifies the passed argument
...
std::string s{"hello"};
...
foo3(s);                    // OK, calls foo3()
foo3(std::move(s));         // ERROR: no matching foo3() declared

std::move() 的头文件
请注意,用 std::move() 标记一个将死的对象是没有意义的。 事实上,这甚至可能对优化产生反作用。
std::move() 在 C++ 标准库中被定义为一个函数。 因此,要使用它,您必须在定义它的位置包含头文件 :


#include <utility> // for std::move()

使用 std::move() 的程序通常编译时不包含这个头文件,因为实际上几乎所有的头文件都包含 。 但是,包含实用程序不需要标准头文件。 因此,在使用 std::move() 时,您应该显式包含 以使您的程序可移植。

std::move() 的实现
std::move() 只不过是对右值引用的静态转换。 您可以通过手动调用 static_cast 来达到相同的效果,如下所示:


foo(static_cast<decltype(obj)&&>(obj)); // same effect as foo(std::move(obj))

因此,我们也可以这样写:


std::string s;
...
foo(static_cast<std::string&&>(s));// same effect as foo(std::move(s))

请注意,static_cast 不仅仅在此处更改对象的类型。 它还允许将对象传递给右值引用(请记住,通常不允许将具有名称的对象传递给右值引用)。 我们将在有关值类别的章节中详细讨论这一点。

在 std::move() 之后,被moved-from的对象不会(部分)被破坏。 它们仍然是至少会调用析构函数的有效对象。 但是,它们也应该是有效的,因为它们具有一致的状态并且所有操作都按预期工作。 你唯一不知道的是它们的值。 这就像使用您不知道传递了哪个值的类型的参数。
C++ 标准库保证moved-from对象处于有效但未指定的状态。
考虑以下代码:


std::string s;
...
coll.push_back(std::move(s));

在使用 std::move() 传递 s 后,您可以询问字符个数、打印值,甚至分配一个新值。 但是,如果不先检查字符个数,则无法打印第一个字符或任何其他字符:


foo(std::move(s));                  // keeps s in a valid but unclear state
std::cout << s << ' \n ' ;            // OK (don’t know which value is written)
std::cout << s.size() << ' \n ' ; // OK (writes current number of characters)
std::cout << s[0] << ' \n ' ;     // ERROR (potentially undefined behavior)
std::cout << s.front() << ' \n ' ;    // ERROR (potentially undefined behavior)
s = "new value";                 // OK

尽管您不知道该值,但该字符串处于一致状态。 例如, s.size() 将返回字符数,以便您可以遍历所有有效索引:


foo(std::move(s));  // keeps s in a valid but unclear state
for (int i = 0; i < s.size(); ++i)
{std::cout << s[i];// OK
}

对于用户定义的类型,您还应该确保moved-from对象处于有效状态,这有时需要声明或实现移动操作。
您可能想知道为什么moved-from的对象仍然是有效的对象并且没有(部分)被破坏。 原因是移动语义有一些有用的应用程序,在这些应用程序中再次使用已移动对象是有意义的。 例如,考虑我们从流中逐行读取字符串并将它们移动到vector中的代码:


std::vector<std::string> allRows;
std::string row;
while (std::getline(myStream, row))     // read next line into row
{ allRows.push_back(std::move(row));    // and move it to somewhere
}

每次将一行读入row后,我们使用 std::move() 将row的值移动到allRows的vector中。 然后, std::getline() 再次使用moved-from的对象row将下一个row读入其中。
作为第二个示例,考虑一个交换两个值的通用函数:


template<typename T>
void swap(T& a, T& b)
{T tmp{std::move(a)};a = std::move(b); // assign new value to moved-from ab = std::move(tmp); // assign new value to moved-from b
}

在这里,我们将 a 的值移动到一个临时对象中,以便之后能够移动分配 b 的值。 被移出的对象 b 然后接收到 tmp 的值,它是 a 的前一个值。 像这样的代码用于排序算法,例如,我们移动不同元素的值以将它们带入排序顺序。 为移出的对象分配新值总是在那里发生。 该算法甚至可能对此类移出对象使用排序标准。
一般来说,移出对象应该是可以被销毁的有效对象(析构函数不应该失败),重用以获取其他值,并支持其类型支持的所有其他操作对象,而无需知道该值。

moved-from对象处于有效但未指定状态的规则通常也适用于直接或间接self-move后的对象。
例如,在下面的语句之后,对象 x 通常是有效的,但它的值是未知的:


x = std::move(x);  // afterwards x is valid but has an unclear value

同样,C++ 标准库为其对象保证了这一点。 用户定义的类型通常也应该提供这种保证,但有时你必须实现一些东西来修复默认生成的 move-from 状态

在类中移动语义
在普通类中移动语义


#include <string>
#include <vector>
#include <iostream>
#include <cassert>class Customer
{private:std::string name;           // name of the customerstd::vector<int> values;   // some values of the customer
public:Customer(const std::string& n):name{n} {assert(!name.empty());}std::string getName() const {return name;}void addValue(int val) {values.push_back(val);}friend std::ostream& operator<< (std::ostream& strm, const Customer& cust) {strm << ' [ ' << cust.name << ": ";for (int val : cust.values) {strm << val << ' ' ;}strm << ' ] ' ;return strm;}
};

这个类有两个成员,一个字符串name和一个int的vector values:


class Customer
{private:std::string name;        // name of the customerstd::vector<int> values; // some values of the customer...
};

两个成员的复制代价都很高

  1. 要复制名称,我们必须为字符串的字符分配内存(除非我们有一个短的name并且字符串是使用小字符串优化 (SSO) 实现的)。
  2. 要复制values,我们必须为vector的元素分配内存。

如果我们有一个字符串vector或另一种非常昂贵的元素类型,它会代价更高。 例如,字符串向量的深拷贝必须为元素的动态数组和每个元素所需的内存分配内存。
好消息是这样的类通常会自动支持移动语义。 从 C++11 开始,编译器通常会生成移动构造函数和移动赋值运算符(类似于自动生成复制构造函数和复制赋值运算符)。
这具有以下影响:
1.按值返回局部的Customer将使用移动语义(如果未优化)。
2.按值传递未名的Customer将使用移动语义(如果未优化)。
3.按值传递临时Customer(例如,由另一个函数返回)将使用移动语义(如果它没有被优化掉)。
4.按值传递标有 std::move() 的 Customer 对象将使用移动语义(如果它没有被优化掉)。
例如:


#include "customer.hpp"
#include <iostream>
#include <random>
#include <utility>    // for std::move()
int main()
{// create a customer with some initial values:Customer c{"Wolfgang Amadeus Mozart" };for (int val : {0, 8, 15}) {c.addValue(val);}std::cout << "c: " << c << ' \n ' ;  // print value of initialized c // insert the customer twice into a collection of customers:std::vector<Customer> customers;customers.push_back(c);           // copy into the vectorcustomers.push_back(std::move(c));   // move into the vectorstd::cout << "c: " << c << ' \n ' ; // print value of moved-from c     std::cout << "customers:\n";        // print all customers in the collection:for (const Customer& cust : customers) {std::cout << " " << cust << ' \n ' ;}
}

在这里,我们创建并初始化了一个customer c(为了避免 SSO,我们使用了一个很长的名称)。 c初始化后,第一个输出如下:


c: [Wolfgang Amadeus Mozart: 0 8 15 ]

然后我们将该customer插入vector中两次:复制一次,移动一次:


customers.push_back(c);             // copy into the vector
customers.push_back(std::move(c));  // move into the vector

之后,c 值的下一个输出通常如下所示:


c: [: ]

在第二次调用 push_back() 时,name和values都被移到了vector的第二个元素中。 但是,不要忘记moved-from的对象处于有效但未指定的状态。 因此,第二个输出可以具有任何值名称和值:
1.它可能仍然具有相同的值:


c: [Wolfgang Amadeus Mozart: 0 8 15 ]

2.它可能具有完全不同的价值:


c: [value was moved away: 0 ]

但是,由于提供了移动语义来优化性能,并且分配不同的值不一定是提高性能的一种方式,因此实现将字符串和vector都设为空是很典型的。

无论如何,我们可以看到为类 Customer 自动启用了移动语义。 出于同样的原因,现在可以保证以下代码很开销更低:


Customer createCustomer()
{Customer c{ ... };...return c; // uses move semantics if not optimized away
}std::vector<Customer> customers;
...
customers.push_back(createCustomer());// uses move semantics

重要的信息是,从 C++11 开始,如果类使用受益于移动语义的成员,它们会自动受益于移动语义。 这些类有:

  1. 如果我们从不再需要源值的创建新对象,则移动成员:move constructor

Customer c1{ ... }
...
Customer c2{std::move(c1)}; // move members of c1 to members of c2
  1. 如果我们从不再需要源值的分配值,则 move 分配成员:move assignment operator

Customer c1{ ... }, c2{ ... };
...
c2 = std::move(c1); // move assign members of c1 to members of c2

请注意,通过显式实现以下改进,这样的类可以从移动语义中进一步受益:
正如刚刚介绍的,编译器可能会自动生成特殊的移动成员函数(移动构造函数和移动赋值运算符)。 但是,有一些限制。 约束是编译器必须假设生成的操作是正确的。 正确的是我们优化了正常的复制行为:我们不是复制成员,而是移动它们,因为不再需要源值。

如果类改变了复制或赋值的行为,那么在优化这些操作时它们可能还必须做一些不同的事情。 因此,当用户声明至少以下特殊成员函数之一时,将禁用自动生成移动操作:
Copy constructor
Copy assignment operator
Another move operation
Destructor

请注意,我写的是“用户声明”。 复制构造函数、复制赋值运算符或析构函数的任何形式的显式声明都会禁用移动语义。 例如,如果我们实现一个什么都不做的析构函数,我们就禁用了移动语义:


class Customer
{...~Customer() { // automatic move semantics is disabled}
};

即使是以下声明也足以禁用移动语义:


class Customer
{...~Customer() = default; // automatic move semantics is disabled
};

明确要求具有其默认行为的析构函数是用户声明的,因此禁用了移动语义。 像往常一样,在这种情况下,复制语义将用作后备。
因此:如果没有特别的需要,不要实现或声明析构函数(一个惊人数量的程序员不遵循的规则)。
这也意味着默认情况下,多态基类禁用了移动语义:


class Base
{...virtual ~Base() { // automatic move semantics is disabled}
};

请注意,这意味着仅对在此基类中声明的成员禁用移动语义。 对于派生类的成员,移动语义仍然是自动生成的(如果派生类没有显式声明一个特殊的成员函数)。 类层次结构中的移动语义对此进行了详细讨论。

请注意,即使生成的复制操作正常工作,生成的移动操作也可能会带来问题。 特别是,在以下情况下您必须小心:

  1. 成员对值有限制
    – 成员的值有限制
    – 成员的值相互依赖
  2. 使用具有引用语义的成员(指针、智能指针……)
  3. 对象还没有默认构造状态
    可能发生的问题是moved-from的对象可能不再有效:不变量可能被破坏,或者对象的析构函数甚至可能失败。 例如,本章中 Customer 类的对象可能突然有一个空名称,尽管我们有断言来避免这种情况。

您可以自己实现特殊的移动成员函数。 执行此操作的方式与实现复制构造函数和赋值运算符的方式大致相同。 唯一的区别是该参数被声明为非常量右值引用,并且在实现内部,您必须指定在何处使用更好的方法来优化通常的复制。
让我们看一个类,它实现了特殊复制和特殊移动成员函数,以打印出对象何时被复制以及何时被移动:


#include <string>
#include <vector>
#include <iostream>
#include <cassert>
class Customer
{private:std::string name;        // name of the customerstd::vector<int> values; // some values of the customer
public:Customer(const std::string& n):name{n} {assert(!name.empty());}std::string getName() const {return name;}void addValue(int val) {values.push_back(val);}friend std::ostream& operator<< (std::ostream& strm, const Customer& cust) {strm << ' [ ' << cust.name << ": ";for (int val : cust.values) {strm << val << ' ' ;}strm << ' ] ' ;return strm;}// copy constructor (copy all members):Customer(const Customer& cust):name{cust.name}, values{cust.values} {std::cout << "COPY " << cust.name << ' \n ' ;}// move constructor (move all members):Customer(Customer&& cust):name{std::move(cust.name)},  // noexcept declaration missingvalues{std::move(cust.values)} {std::cout << "MOVE " << name << ' \n ' ;}// copy assignment (assign all members):Customer& operator = (const Customer& cust) {std::cout << "COPYASSIGN " << cust.name << ' \n ' ;name = cust.name;values = cust.values;return *this;}// move assignment (move all members):Customer& operator = (Customer&& cust) { // noexcept declaration missingstd::cout << "MOVEASSIGN " << cust.name << ' \n ' ;name = std::move(cust.name);values = std::move(cust.values);return *this;}
};

让我们详细看看所有特殊复制/移动成员函数的实现。
复制构造函数实现如下:


class Customer
{private:std::string name;// name of the customerstd::vector<int> values; // some values of the customer
public:...// copy constructor (copy all members):Customer(const Customer& cust) : name{cust.name}, values{cust.values} {std::cout << "COPY " << cust.name << ' \n ' ;}...
};

自动生成的复制构造函数只是复制所有成员。 在我们的实现中,我们只添加了一个特定Customer被复制的打印语句。
移动构造函数实现如下:


class Customer
{private:std::string name;// name of the customerstd::vector<int> values; // some values of the customer
public:...// move constructor (move all members):Customer(Customer&& cust) : name{std::move(cust.name)}, // noexcept declaration missingvalues{std::move(cust.values)} {std::cout << "MOVE " << name << ' \n ' ;}...
};

同样,这是默认生成的移动构造函数对附加打印语句所做的事情。
复制构造函数的主要区别是将参数声明为非常量右值引用,然后移动成员。
请注意这里非常重要的一点:移动语义不会被传递。 当我们用cust的成员初始化成员时,我们必须用 std::move() 标记它们。 没有这个,我们只会复制它们(移动构造函数将具有复制构造函数的性能)。
您可能想知道为什么不传递移动语义。 我们没有声明参数 cust 只接受具有移动语义的对象吗? 但是,请注意这里的语义:当调用者不再需要该值时调用此函数。 在移动构造函数中,我们现在有了要处理的值,我们必须决定在哪里以及需要多长时间。 特别是,我们可能需要多次使用该值,并且不会在第一次使用时丢失它。
因此,移动语义未被传递的事实是一个特性,而不是一个错误。 如果我们要传递移动语义,我们将无法使用通过两次移动语义传递的对象。
例如:


void insertTwice(std::vector<std::string>& coll, std::string&& str)
{coll.push_back(str);           // copy str into collcoll.push_back(std::move(str)); // move str into coll
}

如果 str 的所有使用都隐含地具有移动语义,则 str 的值将在第一次 push_back() 调用时被移走。
这里要学习的重要一课是,被声明为右值引用的参数限制了我们可以传递给该函数的内容,但其行为与该类型的任何其他非 const 对象一样。 我们再次必须指定我们不再需要该值的时间和地点。
附加说明:复制构造函数的 print 语句打印传递的Customer的名称:


Customer(const Customer& cust) : name{cust.name}, values{cust.values}
{std::cout << "COPY " << cust.name << ' \n ' ; // cust.name still there
}

移动构造函数不能使用 cust.name,因为在构造函数的初始化中,值可能已被移走。 我们必须改用新对象的成员:


Customer(Customer&& cust) : name{std::move(cust.name)}, values{std::move(cust.values)}
{std::cout << "MOVE " << name << ' \n ' ; // have to use name (cust.name moved away)
}

请注意,您应该始终使用(有条件的)noexcept 规范来实现移动构造函数,以提高重新分配Customer vector的性能。

让我们谈谈特殊成员函数,特别是具体说明特殊复制和移动成员函数的生成时间和方式。
让我们首先简要看一下特殊成员函数这个术语,因为它以不同的方式使用。 C++ 标准将以下六种操作定义为特殊成员函数:
• Default constructor
• Copy constructor
• Copy assignment operator
• Move constructor (since C++11)
• Move assignment operator (since C++11)
• Destructor
但是,在很多情况下,比如在三规则或五规则中,只讨论五这些操作,因为默认构造函数与其他五(或 C++11 之前的三)操作略有不同。 其他五个操作通常没有声明并且具有更复杂的依赖关系。 因此,请确保您始终了解特殊成员函数的含义(到目前为止,我一直试图避免使用该术语)。

您可以在此表中看到一些基本规则:

  1. 仅当用户未声明其他构造函数时,才会自动声明默认构造函数。
  2. 特殊的复制成员函数和析构函数禁用移动支持。 特殊移动成员函数的自动生成被禁用(除非还声明了移动操作)。 然而,移动对象的请求通常仍然有效,因为复制成员函数被用作备用(除非明确删除特殊的移动成员函数)。
  3. 特殊移动成员函数禁用正常的复制和分配。 复制和其他移动特殊成员函数被删除,因此您只能移动(分配)但不能复制(分配)对象(除非还声明了其他操作)。

正如所写,默认情况下,复制和移动特殊成员函数都是为类生成的。 假设类 Person 的以下声明:


class Person
{...
public:
...
// NO copy constructor/assignment declared
// NO move constructor/assignment declared
// NO destructor declared
};

在这种情况下,可以复制和移动 Person :


std::vector<Person> coll;
Person p{"Tina", "Fox"};
coll.push_back(p);              // OK, copies p
coll.push_back(std::move(p));   // OK, moves p

在声明复制特殊成员函数(或析构函数)时,我们禁用了移动特殊成员函数的自动生成:
假设类 Person 的以下声明:


class Person
{...
public:...// copy constructor/assignment declared:Person(const Person&) = default;Person& operator=(const Person&) = default;// NO move constructor/assignment declared
};

因为回退机制有效,所以复制和移动 Person 可以编译,但移动是作为副本执行的:
因此,用 =default 声明一个特殊的成员函数与根本不声明它是不同的。 复制构造函数和复制赋值是用户声明的,它禁用移动构造和移动赋值,以便移动回退到副本。 声明的析构函数具有相同的效果。

如果您有用户声明的移动语义,则您已禁用复制语义。 复制特殊成员函数被删除。


class Person
{...
public:...// NO copy constructor declared// move constructor/assignment declared:Person(Person&&) = default;Person& operator=(Person&&) = default;
};

在这种情况下,我们有一个只移动类型。 Person 可以移动但不能复制:


std::vector<Person> coll;
Person p{"Tina", "Fox"};
coll.push_back(p);              // ERROR: copying disabled
coll.push_back(std::move(p));   // OK, moves pcoll.push_back(Person{"Ben", "Cook"}); // OK, moves temporary person into coll

同样,用 =default 声明一个特殊的成员函数与根本不声明它是不一样的。 但是,这一次,调用者的后果更加严重:复制对象的尝试将不再编译。

支持移动操作但不支持复制操作的类是有意义的。 您可以使用这种仅移动类型来传递资源的所有权或句柄,而无需共享或复制它们。 在 C++ 标准库中有几个只移动类型(例如,I/O 流类、线程类和 std::unique_ptr<>)。

出于同样的原因,如果将移动构造函数声明为已删除,则无法移动(您已禁用此操作;未使用任何回退)并且无法复制(因为已声明的移动构造函数禁用复制操作):

基于我们刚刚讨论的内容,我们现在知道如何在复制仍然有意义时禁用移动语义。 将特殊移动成员函数声明为已删除通常不是正确的方法,因为它禁用了回退机制。 在提供复制语义的同时禁用移动语义的正确方法是声明其他特殊成员函数之一(复制构造函数、赋值运算符或析构函数)。 我建议您默认复制构造函数和赋值运算符(声明其中一个就足够了,但可能会导致不必要的混淆):

在提供复制语义的同时禁用移动语义的正确方法是声明其他特殊成员函数之一(复制构造函数、赋值运算符或析构函数)。 我建议您默认复制构造函数和赋值运算符(声明其中一个就足够了,但可能会导致不必要的混淆):


lass Customer
{...
public:...Customer(const Customer&) = default;// disable move semanticsCustomer& operator=(const Customer&) = default; // disable move semantics
};

因为没有找到生成的特殊移动成员函数,所以现在复制了一个无名的临时客户,甚至是一个标有 std::move() 的Customer:


std::vector<Customer> customers;
...
customers.push_back(createCustomer());          // OK, falls back to copying
customers.push_back(std::move(customers[0]));   // OK, falls back to copying

但是,通常最好实现特殊的移动成员函数来修复任何问题
生成move操作了。 关于迁移状态的章节将讨论实践中的例子。

请注意,仅声明特殊的复制成员函数会破坏常见的“五规则”。 您必须声明特殊的复制成员函数,但也不能声明特殊的移动成员函数(删除和默认都不起作用,实现它们会使类变得不必要地复杂)。 因此,如果您明确声明复制特殊成员函数只是为了禁用移动语义,请添加一个大注释以确保该声明既不会被特殊移动成员函数的声明删除也不会扩展。

请注意,如果移动语义不可用或已被删除,则不会影响使用该类型的类的移动语义的生成。 生成的默认移动构造函数和赋值运算符逐个成员决定是复制还是移动它。 如果无法进行移动(即使删除了移动操作),也会生成一个副本。

例如,假设以下类:


class Customer
{...
public:...Customer(const Customer&) = default;// copying calls enabledCustomer& operator=(const Customer&) = default; // copying calls enabledCustomer(Customer&&) = delete;// moving calls disabledCustomer& operator=(Customer&&) = delete;// moving calls disabled
};

如果这个类被另一个类中的成员使用:


class Invoice
{std::string id;Customer cust;
public:... // no special member functions
};

生成的移动构造函数将移动 id 字符串但复制Customer cust:


Invoice i;
Invoice i1{std::move(i)};
// OK, moves id, copies cust

现在我们可以总结特殊成员函数的新规则(它们何时生成以及它们的行为方式)。
例如,假设我们有以下派生类:


class MyClass : public Base
{private:MyType value;...
};

这里缺少的一件事是 noexcept 规范,我们将在后面关于 noexcept 的章节中介绍它。 但是,我们将在此处提及相应的保证。

Copy Constructor
当满足以下所有条件时,将自动生成复制构造函数:
• 没有用户声明的移动构造函数
• 没有用户声明的移动赋值运算符
如果生成(隐式或使用 =default),则复制构造函数具有以下行为:


MyClass(const MyClass& obj) noexcept-specifier
: Base(obj), value(obj.value)
{}

生成的复制构造函数首先将源对象传递给基类的最佳匹配复制构造函数。 (请记住,复制构造函数总是在自上而下的基础上调用)。 它更喜欢具有相同声明的复制构造函数(通常声明为 const&),但如果它不可用,它可能会调用下一个最佳匹配构造函数(例如,复制构造函数模板)。 之后,它复制其类的所有成员(再次使用最佳匹配)。

如果所有复制操作(所有基类的复制构造函数和所有成员的复制构造函数)都提供此保证,则生成的复制构造函数声明为 noexcept。
Move Constructor
当满足以下所有条件时,将自动生成移动构造函数:
• 没有用户声明的复制构造函数
• 没有用户声明的复制赋值运算符
• 没有用户声明的移动赋值运算符
• 没有用户声明的析构函数
如果生成(隐式或使用 =default),则移动构造函数具有以下行为:


MyClass(MyClass&& obj) noexcept-specifier
: Base(std::move(obj)), value(std::move(obj.value))
{}

生成的移动构造函数首先将标有 std::move() 的源对象传递给基类的最佳匹配移动构造函数,以传递其移动语义。 最佳匹配的移动构造函数通常是具有相同声明(用 && 声明)的构造函数。 但是,如果它不可用,它可能会调用下一个最佳匹配构造函数(例如,移动构造函数模板甚至是复制构造函数)。 之后,它移动其类的所有成员(再次使用最佳匹配)。

如果所有调用的移动/复制操作(所有基类的复制或移动构造函数和所有成员的复制或移动构造函数)都提供此保证,则生成的移动构造函数声明为 noexcept。

当满足以下所有条件时,将自动生成复制赋值运算符:
• 没有用户声明的移动构造函数
• 没有用户声明的移动赋值运算符


MyClass& operator= (const MyClass& obj) noexcept-specifier
{Base::operator=(obj); // - perform assignments for base class membersvalue = obj.value; // - assign new membersreturn *this;
}

生成的复制赋值运算符首先为传递的源对象调用基类的最佳匹配赋值运算符(请记住,与复制构造函数相比,赋值运算符不是自顶向下调用的;它们调用基类赋值实现的operator(s))。 然后它分配其类的所有成员(再次使用最佳匹配)。

请注意,生成的赋值运算符不会检查对象对其自身的赋值。 如果这很关键,您必须自己实现运算符

此外,如果所有的赋值操作,生成的复制赋值运算符都被声明为 noexcept
(基类成员的分配和新成员的分配)给这个保证

移动赋值运算符
当满足以下所有条件时,将自动生成移动赋值运算符:
• 没有用户声明的复制构造函数
• 没有用户声明的移动构造函数
• 没有用户声明的复制赋值运算符
• 没有用户声明的析构函数
如果生成(隐式或使用 =default),则复制赋值运算符大致具有以下行为:


MyClass& operator= (MyClass&& obj) noexcept-specifier
{Base::operator=(std::move(obj)); // - perform move assignments for base class membersvalue = std::move(obj.value); // - move assign new membersreturn *this;
}

生成的移动赋值运算符首先为传递的源对象调用基类的最佳匹配移动赋值运算符,标记为 std::move() 以传递其移动语义。 之后,它移动分配其类的所有成员(再次使用最佳匹配)。

你可能想知道为什么在对象被 std::move() 标记后我们仍然使用源对象 obj 的成员


Base::operator=(std::move(obj)); // - perform move assignments for base class members

但是,在这种情况下,我们将对象标记为基类的特定上下文,它看不到此类中引入的成员。 因此,派生成员具有有效但未指定的状态,但我们仍然可以使用新成员的值。

生成的赋值运算符也不检查对象对其自身的赋值。 因此,在其默认行为中,operator会将每个成员move assign给自己,这通常意味着成员接收到一个有效但未指定的值。 如果这很关键,您必须自己实现运算符。

此外,如果所有调用的赋值操作(基类成员的赋值和新成员的赋值)都提供此保证,则生成的移动赋值运算符声明为 noexcept。

其他特殊的成员函数对移动语义没有那么重要的作用:
析构函数与移动语义没有什么特别之处,只是它们的声明禁用了移动操作的自动生成。

如果没有声明其他构造函数,仍会自动生成默认构造函数(“not-so-special”的特殊成员函数)。 也就是说,移动构造函数的声明禁用了默认构造函数的生成。

三五原则
是否自动生成以及自动生成哪些特殊成员函数取决于刚刚描述的几个规则的组合。 许多程序员并不知道所有这些规则。 因此,即使在 C++11 之前,通常的指导方针是不提供或提供所有用于复制、赋值和销毁的特殊成员函数。

  1. 在 C++11 之前,该准则被称为“三法则”:该准则是要么声明所有三个(复制构造函数、赋值运算符和析构函数),要么不声明它们。
  2. 自 C++11 以来,该规则已成为五规则,通常表述为: 准则是声明所有五项(复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符和析构函数)或不声明。

在这里,声明的意思是:

  1. 要么实现 ({…})
  2. 或声明为默认值 (=default)
  3. 或声明为已删除 (=delete)
    也就是说,当这些特殊成员函数之一被实现或默认或删除时,您应该实现或默认或删除所有其他四个特殊成员函数
    但是,您应该小心这条规则。 我建议您在其中一个是用户声明的时候仔细考虑所有这五个特殊成员函数的指导。

正如我们所看到的,要仅启用复制语义,您应该 =default 复制特殊成员函数而不声明特殊 move 成员函数(删除和默认特殊 move 成员函数将不起作用,实现它们会使类变得不必要地复杂)。 如果生成的移动语义创建了无效状态,则特别推荐使用此选项,正如我们在无效移动状态部分中讨论的那样。

When You Cannot Avoid Using Names
在某些情况下,您无法避免使用std::move(),因为您必须给对象命名。最明显的典型例子是:

你必须多次使用一个对象。例如,你可能会得到一个值来在函数或循环中处理它两次:


std::string str{getData()};
...
coll1.push_back(str);            // copy (still need the value of str)
coll2.push_back(std::move(str)); // move (no longer need the value of str)

当两次将值插入同一个集合或调用两个不同的函数将值存储在某个地方时,情况也是如此。
你必须处理一个参数。最常见的例子是下面的循环:


// read and store line by line from myStream in coll
std::string line;
while (std::getline(myStream, line))
{coll.push_back(std::move(line)); // move (no longer need the value of line)
}

Avoid Unnecessary std::move()
如我们所见,如果支持,按值返回local对象会自动使用move语义。然而,为了安全起见,程序员可能会尝试使用显式的std::move()强制执行:


std::string foo()
{std::string s;...return std::move(s); // BAD: don’t do this
}

请记住,std::move()只是一个静态转换到右值引用。因此,std::move(s)是一个生成类型std::string&&的表达式。但是,这不再与返回类型匹配,因此禁用了返回值优化,该优化通常允许将返回对象用作返回值。对于没有实现move语义的类型,这甚至可能强制复制返回值,而不是仅仅使用返回对象作为返回值。

因此,如果你通过值返回本地对象,不要使用std::move():


std::string foo()
{std::string s;... return s; // best performance (return value optimization or move)
}

在已有临时对象的情况下使用std::move()至少是多余的。对于createString()函数按值返回对象,你应该只使用返回值:


std::string s{createString()}; // OK

而不是再次使用std::move()来标记它:


std::string s{std::move(createString())}; // BAD: don’t do this

编译器可能(有选项)对任何适得其反或不必要的std::move()使用发出警告。例如,gcc有选项-wpessimisization -move(启用了-Wall)和-wredundancy -move(启用了-Wextra)。
不过,在某些应用程序中,返回语句中的std::move()可能是合适的。

Initialize Members with Move Semantics

一个令人惊讶的结果是,您甚至可以从移动语义中受益,即使是在具有受益于移动语义的类型(如字符串成员或容器)的普通类中。让我们看一个简单的例子

Initialize Members the Classical Way
考虑一个具有两个string成员的类,我们可以在构造函数中初始化它们。这样的类通常会这样实现:


#include <string>
class Person
{private:std::string first; // first namestd::string last;  // last name
public:Person(const std::string &f, const std::string &l): first{f}, last{l} {}...
};

现在让我们看看当我们用两个字符串字面值初始化这个类的对象时会发生什么:


Person p{"Ben", "Cook"};

编译器发现提供的构造函数可以执行初始化。但是,参数的类型不匹配。因此,编译器生成代码,首先创建两个临时的std::string,它们由两个字符串字面值的值初始化,并将参数f和l绑定到它们:

通常(如果小字符串优化(SSO)不可用或字符串太长),这意味着生成代码为每个std::string的值分配内存。
但是,创建的临时字符串不会首先或最后直接作为成员使用。相反,它们被用来初始化这些成员。不幸的是,这里没有使用move语义,原因有二:

  1. 形参f和l是具有比成员初始化时间更长的名称的对象(您仍然可以在构造函数体中使用它们)。
  2. 形参被声明为const,这样即使使用std::move()也禁用了move语义。
    结果,字符串的复制构造函数在每个成员初始化时被调用,同样为值分配内存:

在构造函数的末尾,销毁临时字符串:

这意味着我们有四个内存分配,尽管只有两个是必要的。使用移动语义我们可以做得更好。

Using non-const Lvalue References?
你可能想知道为什么不能简单地使用非const左值引用:


class Person
{... Person(std::string &f, std::string &l): first{std::move(f)}, last{std::move(l)} {}...
};

但是,传递const std::string和临时对象(例如,通过类型转换创建)将无法编译:


Person p{"Ben", "Cook"}; // ERROR: cannot bind a non-const lvalue reference to a temporary

一般来说,非const左值引用不绑定到临时对象。因此,该构造函数不能将f和l绑定到从传递的字符串字面量创建的临时字符串。

Initialize Members via Moved Parameters Passed by Value
有了move语义,现在构造函数有了一种简单的替代方法来初始化成员:构造函数按值接受每个参数,并将其移动到成员中:


#include <string>
class Person
{private:std::string first; // first namestd::string last;  // last name
public:Person(std::string f, std::string l): first{std::move(f)}, last{std::move(l)} {}...
};

这个构造函数接受所有可能的参数,并确保每个参数只有一个分配。
例如,如果我们传递两个字符串字面值:


Person p{"Ben", "Cook"};

我们首先使用它们来初始化参数f和l:

通过使用std::move(),我们可以将参数的值移动到成员中。首先,成员首先从f中窃取值:

然后,成员最后一次从l中窃取值:

同样,在构造函数的末尾,销毁临时字符串。这次它花费的时间更少,因为字符串的析构函数不再需要释放已分配的内存:

如果传入std::strings,则这种初始化成员的方式也可以正常工作:
如果传入两个现有字符串,但不使用std::move()进行标记,则将名称复制到形参中,并将其移动到成员中:


std::string name1{"Jane"}, name2{"White"};
...
Person p{name1, name2}; // OK, copy names into parameters and move them to the members

如果传入两个不再需要该值的字符串,则完全不需要分配:


std::string firstname{"Jane"};
...
Person p{std::move(firstname), // OK, move names via parameters to membersgetLastnameAsString()};

在本例中,我们移动了传递的字符串两次:一次是初始化参数f和l,一次是将f和l的值移动到成员中。
只要移动是廉价的,这种只有一个构造函数的实现就可以进行任何初始化,而且是廉价的。

Initialize Members via Rvalue References
还有更多的方法来初始化Person的成员,使用多个构造函数。
Using Rvalue References
为了支持move语义,我们已经学过可以将形参声明为非const右值引用。这允许参数从传递的临时对象或标记为std::move()的对象中窃取值。
考虑这样声明构造函数:


class Person
{... Person(std::string &&f, std::string &&l): first{std::move(f)}, last{std::move(l)} {}...
};

这个初始化也适用于我们传递的字符串字面值:


Person p{"Ben", "Cook"};

同样,由于构造函数需要字符串,我们将创建两个临时字符串,f和l将其绑定:

因为我们有非const引用,所以我们可以修改它们。在本例中,我们使用std::move()标记它们,以便成员的初始化可以窃取值。
首先,成员首先从f中窃取值:

然后,成员最后一次从l中窃取值:

同样,在构造函数的末尾,销毁临时字符串而不需要释放已分配的内存:

Overloading for Rvalue and Lvalue References

虽然接受右值引用的函数在向它们传递临时对象时工作得很好(这里,我们传递的是由字符串字面量创建的临时std::string),但它们也有限制:不能传递命名对象,但事后仍然需要该值。因此,如果我们只有一个接受右值引用的构造函数,我们就不能传递一个现有的字符串:


class Person
{... Person(std::string &&f, std::string &&l): first{std::move(f)}, last{std::move(l)} {}...
};
Person p1{"Ben", "Cook"}; // OK
std::string name1{"Jane"}, name2{"White"};
...
Person p2{name1, name2}; // ERROR: can’t pass a named object to an rvalue reference

对于p2,我们需要一个传统的构造函数,它是用一个const左值引用声明的。但是,我们也可以传递一个字符串字面值和一个现有的字符串。因此,对于所有可能的组合,我们总共需要4个构造函数:


class Person
{... Person(const std::string &f, const std::string &l): first{f}, last{l}{}Person(const std::string &f, std::string &&l): first{f}, last{std::move(l)}{}Person(std::string &&f, const std::string &l): first{std::move(f)}, last{l}{}Person(std::string &&f, std::string &&l): first{std::move(f)}, last{std::move(l)} {}...
};

这样,我们就可以同时传递字符串字面值和现有字符串的任意组合,并且每个成员总是只有一次分配

Overloading Even for String Literals

为了进一步提高性能,我们甚至可以使用特定的实现,将字符串字面量作为普通指针。这样的话,我们甚至可以避免一些动作。然而,实现所有构造函数变得有点乏味:


#include <string>
class Person
{private:std::string first; // first namestd::string last;  // last name
public:Person(const std::string &f, const std::string &l): first{f}, last{l}{}Person(const std::string &f, std::string &&l): first{f}, last{std::move(l)}{}Person(std::string &&f, const std::string &l): first{std::move(f)}, last{l}{}Person(std::string &&f, std::string &&l): first{std::move(f)}, last{std::move(l)}{}Person(const char *f, const char *l): first{f}, last{l}{}Person(const char *f, const std::string &l): first{f}, last{l}{}Person(const char *f, std::string &&l): first{f}, last{std::move(l)}{}Person(const std::string &f, const char *l): first{f}, last{l}{}Person(std::string &&f, const char *l): first{std::move(f)}, last{l} {}...
};

这种解决方案的好处在于,我们减少了移动的次数。如果传递一个字符串字面值,则直接使用传递的指针初始化成员,而不是创建std::string并将其值移动到成员

Compare the Different Approaches
随着初始化成员的新方法的引入,一个明显的问题是,我们应该在什么时候使用哪种技术?还有:我们应该教什么技巧?通常,有很好的理由不使用简单的方法与const&。原因通常是性能。因此,让我们用三种参数来测量初始化person所需的时间:传递字符串字面值,传递现有字符串,以及传递用std::move()标记的字符串。


std::string fname = "a first name";
std::string lname = "a last name";
// measure how long this takes:
Person p1{"a firstname", "a lastname"};
Person p2{fname, lname};
Person p3{std::move(fname), std::move(lname)};

然而,为了避免小字符串优化(SSO),这意味着字符串根本不分配任何内存,我们应该使用长度较大的字符串。因此,这里有一个完整的函数来衡量不同的方法:


#include <chrono>
// measure num initializations of whatever is currently defined as Person:
std::chrono::nanoseconds measure(int num)
{std::chrono::nanoseconds totalDur{0};for (int i = 0; i < num; ++i){std::string fname = "a firstname a bit too long for SSO";std::string lname = "a lastname a bit too long for SSO";// measure how long it takes to create 3 Persons in different ways:auto t0 = std::chrono::steady_clock::now();Person p1{"a firstname too long for SSO", "a lastname too long for SSO"};Person p2{fname, lname};Person p3{std::move(fname), std::move(lname)};auto t1 = std::chrono::steady_clock::now();totalDur += t1 - t0;}return totalDur;
}

函数measure()返回执行上述三次初始化的num迭代的持续时间,且字符串的长度相当。现在,我们将上面Person类的不同定义与程序中的测量函数结合起来,使用main()函数调用measure并打印结果durations。例如:


#include "initclassic.hpp"
#include "initmeasure.hpp"
#include <iostream>
#include <cstdlib> // for std::atoi()
int main(int argc, const char **argv)
{int num = 1000; // num iterations to measureif (argc > 1){num = std::atoi(argv[1]);}// a few iterations to avoid measuring initial behavior:measure(5);// measure (in integral nano- and floating-point milliseconds):std::chrono::nanoseconds nsDur{measure(num)};std::chrono::duration<double, std::milli> msDur{nsDur};// print result:std::cout << num << " iterations take: "<< msDur.count() << "ms\n";std::cout << "3 inits take on average: "<< nsDur.count() / num << "ns\n";
}

在三个不同的平台上使用三个不同的编译器运行这段代码的效果如下:
•通常,使用经典的左值引用(const &)的初始化比其他初始化要花费更多的时间。我见过最多2的因数。
•实现所有9个构造函数和构造函数只接受参数的值和移动之间没有太大的区别。

您可能有一些开销很大的成员,它们不能从move语义中受益(例如包含10000个双精度值的数组):


class Person
{private:std::string name;std::array<double, 10000> values; // move can’t optimize here
public:...
};

在这种情况下,我们会遇到一个问题,即初始参数为10000倍的value和move。我们必须复制参数两次,这花费了几乎两倍的时间。

Summary for Member Initialization
作为一个总结,要初始化移动语义产生显著差异的成员(字符串,容器,或具有此类成员的类/数组),你应该在以下选项中使用移动语义:
从通过左值引用获取形参转换为通过值获取形参,并将其移到成员中
第一个选项允许我们只有一个构造函数,这样代码更容易维护。然而,这确实会导致不必要的更多移动操作。因此,如果移动操作可能花费大量时间,那么最好使用多个重载。例如,如果我们有一个包含字符串和值向量的类,按值取和移动通常是正确的方法


class Person
{private:std::string name;std::vector<std::string> values;public:Person(std::string n, std::vector<std::string> v): name{std::move(n)}, values{std::move(v)} {}...
};

但是,如果我们有一个std::array成员,最好重载,因为即使移动了成员,移动std::array仍然需要大量的时间:


class Person
{private:std::string name;std::array<std::string, 1000> values;
public:Person(std::string n, const std::array<std::string, 1000> &v): name{std::move(n)}, values{v}{}Person(std::string n, std::array<std::string, 1000> &&v): name{std::move(n)}, values{std::move(v)} {}...
};

Should We Now Always Pass by Value and Move?
上面的讨论引出了这样一个问题:现在是否应该始终按值接受参数,并将它们移动到设置内部值或成员的位置。答案是否定的。这里讨论的特殊情况是创建并初始化一个新值。在这种情况下,这种策略是有效的。如果我们已经有了一个值,我们更新或修改它,使用这种方法将会适得其反。一个简单的例子就是setter。考虑个人类的以下实现:


class Person
{private:std::string first; // first namestd::string last;  // last name
public:Person(std::string f, std::string l): first{std::move(f)}, last{std::move(l)} {}... void setFirstname(std::string s){                         // take by valuefirst = std::move(s); // and move}...
};

假设我们使用这样的类:


Person p{"Ben", "Cook"};
std::string name1{"Ann"};
std::string name2{"Constantin Alexander"};
p.setFirstname(name1);
p.setFirstname(name2);
p.setFirstname(name1);
p.setFirstname(name2);

每次设置新firstname时,都会创建一个新的临时参数s,该参数分配自己的内存,然后将其移动到成员的值。因此,我们有四个分配(假设我们没有SSO)。现在考虑一下,我们以传统的方式实现setter,接受一个const左值引用:


class Person
{private:std::string first; // first namestd::string last;  // last name
public:Person(std::string f, std::string l): first{std::move(f)}, last{std::move(l)} {}... void setFirstname(const std::string &s){              // take by referencefirst = s; // and assign}...
};

绑定到传递的参数不会创建一个新的字符串。此外,只有当新长度超过为该值分配的当前内存量时,赋值操作符才会分配新的内存。这意味着,因为我们已经有了一个值,通过值和移动来获取参数的方法可能会适得其反。
你可能想知道是否重载setter,以便在新长度超过现有长度时从move语义中获益:


class Person
{private:std::string first; // first namestd::string last;  // last name
public:Person(std::string f, std::string l): first{std::move(f)}, last{std::move(l)} {}... void setFirstname(const std::string &s){              // take by lvalue referencefirst = s; // and assign}void setFirstname(std::string &&s){                         // take by rvalue referencefirst = std::move(s); // and move assign}...
};

然而,即使是这种方法也可能适得其反


Person p{"Ben", "Cook"};
p.setFirstname("Constantin Alexander"); // would allocate enough memory
p.setFirstname("Ann"); // would reduce capacity
p.setFirstname("Constantin Alexander"); // would have to allocate again

即使使用move语义,设置现有值的最佳方法是通过const左值引用获得新值,并且不使用std::move()进行赋值。按值接受参数并将其移动到需要新值的地方,只有当我们将传递的值作为新值存储在某处时才有用(因此无论如何我们都需要为它增加新的内存)。在修改现有值时,此策略可能适得其反。然而,初始化成员并不是“take by value and move.”。向容器添加新值的(成员)函数是另一个方法


class Person
{private:std::string name;std::vector<std::string> values;public:Person(std::string n, std::vector<std::string> v): first{std::move(n)}, values{std::move(v)} {}...// better pass by value and move to create a new element:void addValue(std::string s){                                   // take by valuevalues.push_back(std::move(s)); // and move into the collection}...
};

Move Semantics in Class Hierarchies
正如我们所看到的,任何复制构造函数、复制赋值或析构函数的声明都会禁用对move语义的自动支持。这也适用于多态基类。但是,还有一些额外的方面需要考虑。

多态基类通常引入虚成员函数,您可以为派生类的所有对象调用虚成员函数。例如:


class GeoObj
{public:virtual void draw() const = 0;   // pure virtual function (introducing the API)... virtual ~GeoObj() = default;     // let delete call the right destructor...                              // other special member functions due to the problem of slicing
};

在这个基类中,移动语义是禁用的,这意味着如果我们移动一个geometric对象,基类中声明的成员不会自动支持移动语义。如果我们有一个受保护的复制构造函数和一个被删除的赋值操作符,这也适用,你通常应该在多态基类中使用,以避免切片的问题。

只要基类不引入成员,不支持move语义是没有作用的。但是,如果在这个基类中有一个开销很大的成员,那么您就禁用了对它的移动支持。例如:


class GeoObj
{protected:std::string name; // name of the geometric object
public:... virtual void draw() const = 0;      // pure virtual function (introducing the API)... virtual ~GeoObj() = default;        // disables move semantics for name...                                 // other special member functions due to the problem of slicing
};

若要再次启用移动语义,可以将移动操作显式声明为默认值。但是,正如我们刚刚了解到的,这将禁止复制特殊成员函数。因此,如果您想要拥有这些函数,就必须显式地提供它们

处理切片
但是,有一个切片的问题。考虑下面的代码,使用基类GeoObj作为派生类Circle的对象的引用:


Circle c1{ ... }, c2{ ... };
GeoObj& geoRef{c1};
geoRef = c2; // OOPS: uses GeoObj::operator=() and assigns no Circle members

因为我们调用GeoObj的赋值操作符,而且该操作符不是虚的,所以编译器调用GeoObj::operator=(),它不处理任何派生类的任何成员。即使用virtual声明赋值操作符也没有帮助,因为派生类的操作符不重写基类的赋值操作符(第二个操作数的形参类型不同)。

为了避免这个问题,您应该在多态类层次结构中禁用赋值操作符。此外,如果类不是抽象类,还应该避免使用公共复制构造函数来禁用到基类的隐式类型转换。因此,具有move语义(和成员)的多态基类应声明如下:


class GeoObj
{protected:std::string name; // name of the geometric objectGeoObj(std::string n): name{std::move(n)}{}
public:virtual void draw() const = 0;   // pure virtual function (introducing the API)... virtual ~GeoObj() = default; // would disable move semantics for name
protected:// enable copy and move semantics (callable only for derived classes):GeoObj(const GeoObj &) = default;GeoObj(GeoObj &&) = default;// disable assignment operator (due to the problem of slicing):GeoObj &operator=(GeoObj &&) = delete;GeoObj &operator=(const GeoObj &) = delete;
};

实现一个多态派生类
多态派生类可能如下所示


class Polygon : public GeoObj
{protected:std::vector<Coord> points;
public:Polygon(std::string s, std::initializer_list<Coord> = {}); // constructorvirtual void draw() const override;                        // implementation of draw()
};

通常,在多态派生类中不需要声明特殊的成员函数。特别是,不需要再次声明虚析构函数(除非必须实现它)。再次声明析构函数(无论是否为虚函数)将禁用对派生类成员(这里是向量点)的自动move语义支持:


class Polygon : public GeoObj
{protected:std::vector<Coord> points;
public:Polygon(std::string s, std::initializer_list<Coord> = {}); // constructor... virtual ~Polygon() = default;                          // OOPS: don’t do that because it disables move semantics
};

然而,在没有声明析构函数的情况下,move语义对Polygon成员、名称和点都有效。考虑以下程序:


#include "geoobj.hpp"
#include "polygon.hpp"
int main()
{Polygon p0{"Poly1", {Coord{1, 1}, Coord{1, 9}, Coord{9, 9}, Coord{9, 1}}};Polygon p1{p0};            // copyPolygon p2{std::move(p0)}; // movep0.draw();p1.draw();p2.draw();
}

这个程序有以下输出:


polygon ’’ over
polygon ’TestPolygon’ over (1,1) (1,9) (9,9) (9,1)
polygon ’TestPolygon’ over (1,1) (1,9) (9,9) (9,1)

对于成员、名称和点,值都从p0移动到p2。

重载引用限定符

Return by Value
按值返回的getter应该是这样的(记住:不要用const返回值,否则你会禁用move语义):


class Person
{private:std::string name;
public:... std::string getName() const{return name;}
};

这段代码是安全的,但每次查询name时,我们都可能复制name
例如,只是检查我们是否有Person的name是空的,这将是有意义的:


std::vector<Person> coll;
...
for (const auto &person : coll)
{if (person.getName().empty()){ // OOPS: copies the namestd::cout << "found empty name\n";}
}

如果将这种方法与返回引用的方法进行比较,可以看到按值返回字符串的版本的性能开销在2到100之间(前提是名称的长度较大,因此SSO没有帮助)。如果访问的成员是一个图像或数千个元素的集合,则可能更糟糕。在这种情况下,getter通常通过(const)引用返回以提高性能。

Return by Reference
通过引用返回的getter应该如下所示:


class Person
{private:std::string name;
public:... const std::string &getName() const{return name;}
};

这更快,但有些不安全,因为调用者必须确保返回引用引用的对象存活足够长的时间。事实上,使用getter的返回值的时间比调用getter的对象的时间长是有生命周期风险的。陷入这种陷阱的一种方法是使用基于范围的for循环,如下所示:


for (char c : returnPersonByValue().getName())
{ // OOPS: undefined behaviorif (c == ' '){...}
}

注意,在循环头的右侧有一个函数,它返回一个临时对象,我们用getter引用这个对象。然而,基于范围的for循环的定义使上面的代码等价于以下代码:


reference range = returnPersonByValue().getName();
// OOPS: returned temporary object destroyed here
for (auto pos = range.begin(), end = range.end(); pos != end; ++pos)
{char c = *pos;if (c == ' '){...}
}

在开始迭代之前,我们要初始化一个reference1,因为我们必须使用传递的范围两次(一次调用begin(),一次调用end()),并希望避免创建范围的副本(这可能很昂贵,甚至不可能)。通常,引用会延长所引用对象的生命周期。但是,在这种情况下,range不指向由returnPersonByValue()返回的Person;range指的是getName()的返回值,它是对返回的Person的引用。因此,range扩展了引用的生命周期,但不扩展引用所引用的临时对象的生命周期。因此,随着第一个语句的结束,返回的临时对象将被销毁,并且在遍历已销毁对象的字符时,使用对已销毁对象名称的引用。最好的情况是,我们在这里有一个核心转储,这样我们就能看到一些明显的错误。在最坏的情况下,一旦我们发布了软件,我们就会得到致命的未定义行为。如果getter按值返回名称,这样的代码就不是问题。在这种情况下,range将扩展名称副本的生存期,以便我们可以使用该名称直到range的生存期结束

Using Move Semantics to Solve the Dilemma
有了move语义,我们现在有了解决这一困境的方法。如果这样做是安全的,则可以通过引用返回;如果可能遇到生命周期问题,则可以通过值返回。编程的方法如下:


class Person
{private:std::string name;
public:... std::string getName() &&{                           // when we no longer need the valuereturn std::move(name); // we steal and return by value}const std::string &getName() const &{                // in all other casesreturn name; // we give access to the member}
};

我们用不同的引用限定符重载getter,就像重载一个包含&&和构造&形参的函数一样:
带有&&限定符的版本用于当我们有一个不再需要该值的对象(一个即将死亡的对象或我们已经用std::move()标记的对象)时。
带有const&qualifier的版本用于所有其他情况。它总是适合的,但如果我们不能采用&&版本,这只是一种退路。因此,如果有一个对象不会死亡或标记为std::move(),则使用此函数。
现在我们的性能和安全性都很好:


Person p{"Ben"};
std::cout << p.getName();                     // 1) fast (returns reference)
std::cout << returnPersonByValue().getName(); // 2) fast (uses move())
std::vector<Person> coll;
...
for (const auto &person : coll)
{if (person.getName().empty()){ // 3) fast (returns reference)std::cout << "found empty name\n";}
}
for (char c : returnPersonByValue().getName())
{ // 4) safe and fast (uses move())if (c == ' '){...}
}

语句1)和语句3)使用了constor &的版本,因为我们有一个对象,其名称没有使用std::move()标记。语句2)和语句4)使用&&的版本,因为我们对临时对象调用getName()。因为临时对象即将死亡,getter可以将成员名移出作为返回值,这意味着我们不必为返回值分配新的内存;我们从成员那里窃取值

您可能还记得,return语句不应该使用std::move()来移出无论如何都会死亡的局部对象。但是,在这种情况下,我们没有返回局部对象;返回一个成员,其生存期不会随着成员函数的结束而结束

std::move() for Calling Member Functions
请注意,这个特性意味着即使在调用成员函数时也值得使用std::move()。例如:


void foo()
{Person p{...};... coll.push_back(p.getName());            // calls getName() const&... coll.push_back(std::move(p).getName()); // calls getName() && (OK, p no longer used)
}

在调用getName()时使用std::move()可以提高程序的性能。返回的不是只能复制的const std::string类型的引用,而是p的移动名作为非const字符串返回,以便push_back()可以使用move语义将其移动到coll中。与往常一样,在这个调用之后,p处于有效但未指定的状态。有关在c++标准库中使用此特性的示例,请参阅类std::optional<>。

Overloading on Qualifiers
自c++ 98以来,可以重载实现const和非const版本的成员函数。例如:


class C
{public:... void foo();   // foo() for non-const objectsvoid foo() const; // foo() for const objects
};

圆括号后面的限定符允许我们限定一个没有传递给形参的对象:为其调用此成员函数的对象。现在,有了move语义,我们有了用限定符重载函数的新方法,因为我们有不同的引用限定符。考虑以下程序:


#include <iostream>
class C
{public:void foo() const &{std::cout << "foo() const&\n";}void foo() &&{std::cout << "foo() &&\n";}void foo() &{std::cout << "foo() &\n";}void foo() const &&{std::cout << "foo() const&&\n";}
};int main()
{C x;x.foo();            // calls foo() &C{}.foo();          // calls foo() &&std::move(x).foo(); // calls foo() &&const C cx;cx.foo();            // calls foo() const&std::move(cx).foo(); // calls foo() const&&
}

这个程序演示了所有可能的引用限定符以及它们被调用的时间。通常,我们只有两到三个这样的重载,例如对getter使用&&和const&(和&)。
还要注意,不允许重载引用限定符和非引用限定符:


class C
{public:void foo() &&;void foo() const; // ERROR: can’t overload by both reference and value qualifiers
};

When to Use Reference Qualifiers
引用限定符允许我们在为特定值类别的对象调用函数时以不同的方式实现函数。目的是在为不再需要其值的对象调用成员函数时提供不同的实现。虽然我们确实有这个功能,但它并没有得到尽可能多的使用。特别是,我们可以(也应该)使用它来确保修改对象的操作不会被即将死亡的临时对象调用。

Reference Qualifiers for Assignment Operators
更好地使用引用限定符的一个例子是修改赋值操作符的实现。正如在http://wg21.link/n2819 中建议的那样,在任何可能的地方使用引用限定符声明赋值操作符可能会更好。
例如,字符串的赋值操作符声明如下:


namespace std
{template <typename charT, ...>class basic_string{public:... constexpr basic_string &operator=(const basic_string &str);constexpr basic_string &operator=(basic_string &&str) noexcept(...);constexpr basic_string &operator=(const charT *s);...};
}

这样就可以将新值意外赋给临时字符串:


std::string getString();
getString() = "hello"; // OK
foo(getString() = ""); // passes string instead of bool

考虑使用引用限定符声明赋值操作符:


namespace std
{template <typename charT, ...>class basic_string{public:... constexpr basic_string &operator=(const basic_string &str) &;constexpr basic_string &operator=(basic_string &&str) &noexcept(...);constexpr basic_string &operator=(const charT *s) &;...};
}

这样的代码将不再编译:


std::string getString();
getString() = "hello"; // ERROR
foo(getString() = ""); // ERROR

请注意,特别是对于可以用作布尔值的类型,这将有助于发现像以下这样的bug:


std::optional<int> getValue();
if (getValue() = 0)
{ // OOPS: compiles although = is used instead of ==...
}

本质上,我们给临时对象返回一个它们用于基本类型的属性:它们是右值,这意味着它们不能在赋值的左边。
注意,所有这些修改c++标准的建议都被拒绝了。主要原因是向后兼容性的考虑。但是,在实现自己的类时,可以如下使用这种改进:


class MyType
{public:...// disable assigning value to temporary objects:MyType &operator=(const MyType &str) & = default;MyType &operator=(MyType &&str) & = default;// because this disables the copy/move constructor, also:MyType(const MyType &) = default;MyType(MyType &&) = default;...
};

一般来说,对于可能修改对象的每个成员函数都应该这样做

Reference Qualifiers for Other Member Functions
正如getter示例所演示的那样,当返回对对象的引用时,也可以且应该使用引用限定符。这样,就可以减少访问已销毁临时对象成员的风险
同样,标准字符串的当前声明可以作为一个例子:


namespace std
{template <typename charT, ...>class basic_string{public:... constexpr const charT &operator[](size_type pos) const;constexpr charT &operator[](size_type pos);constexpr const charT &at(size_type n) const;constexpr charT &at(size_type n);constexpr const charT &front() const;constexpr charT &front();constexpr const charT &back() const;constexpr charT &back();...};
}

相反,下面的重载会更好:


namespace std
{template <typename charT, ...>class basic_string{public:... constexpr const charT &operator[](size_type pos) const &;constexpr charT &operator[](size_type pos) &;constexpr charT operator[](size_type pos) &&;constexpr const charT &at(size_type n) const &;constexpr charT &at(size_type n) &;constexpr charT at(size_type n) &&;constexpr const charT &front() const &;constexpr charT &front() &;constexpr charT front() &&;constexpr const charT &back() const &;constexpr charT &back() &;constexpr charT back() &&;...};
}

同样,由于向后兼容性,c++标准中的相应更改可能会成为一个问题。但是,您可以并且应该为您的类型提供这些重载。在这种情况下,不要忘记右值引用的实现应该移出昂贵的成员。

Moved-From States

尽管生成或简单实现的移动语义通常工作得很好,但我们至少应该看看可能出现的情况,即移动操作将对象带入c++标准库不支持的状态,或破坏类型的不变量。在本章中,我们根据c++标准库的保证来澄清“无效”状态的定义,即从对象中移出的对象处于有效但未指定的状态。

Required and Guaranteed States of Moved-From Objects
在移动操作之后,被移动的对象既不会部分销毁,也不会完全销毁。析构函数还没有被调用,并且在被move-from对象的生命周期结束时仍将被调用。因此,析构函数至少要平稳运行。然而,c++标准库为其移出类型提供了更多保障。从移动对象处于“有效但未指定的状态”。这意味着您可以使用move-from对象,就像使用其类型的任何对象一样,但您不知道它的值。这就像使用该类型的非const引用形参,而不知道所传递对象的值。通过知道我们可以做的不仅仅是销毁从移动对象,例如,我们可以使用移动语义来实现排序和变化算法。为了更详细地理解如何处理从移动对象,最好区分与它们相关的两个方面
•在c++标准库中安全地使用move-from对象的要求是什么?
•你应该给你的类型的move-from对象什么保证,以便这些类型的用户知道如何使用它们的良好定义的行为?通常,你给出的保证至少应该满足c++标准库的要求,但它们可能提供更多的保证。

Required States of Moved-From Objects
对于c++标准库中的移出对象的要求并没有什么特别的。也就是说,对于任何函数,它为传递的类型和对象制定的需求也适用于任何从内部传递或使用的对象中移动出来的对象。基本上,你总是需要能够销毁一个移动的对象。此外,在许多函数中,必须能够将新值赋给从move-from对象。例如,考虑如何交换两个对象a和b的值(这可能是排序操作的一部分)。交换通常是这样实现的(参见图6.1):
•将a移动到一个新的临时对象tmp(使a成为一个movefromobject)。
•将b移动赋值给从移动对象a(使b成为从移动对象)。
•将tmp赋值给从对象b(使tmp成为一个从对象)
•删除被移动的对象tmp

请注意,通过将同一个对象传递给两个参数,也可以实现自交换。在这种情况下,我们甚至可以将状态为move-from的对象赋值给它自己。这一切应该都可以工作(前提是一个move-from对象仍然可以有任何值)。因此,对于move-from对象,我们有通常适用于所有对象的基本要求:
•我们必须能够摧毁移动的物体。
•我们必须能够分配一个新值从对象移动。
•我们应该能够复制、移动或赋值从一个对象到另一个对象。从对象移出还应该能够处理特定操作的附加需求。例如,要对对象进行排序,我们必须支持对所有对象调用operator<或排序标准。这也适用于从对象移出。你可能会说在你的排序算法中,你应该知道哪个对象被移动了,这样你就可以避免比较它,但是c++标准库不需要这样做。顺便说一下,这也意味着您可以传递“从移动对象”来对它们进行排序。只要它们支持所有必需的操作,就没有问题。

Guaranteed States of Moved-From Objects
从移动对象的保证定义了在使用它们时哪些代码是定义良好的。通常,类的设计者决定给出哪些保证。但是,因为我们总是在一个对象的生命周期结束时销毁它。对于move-from状态,您必须保证调用析构函数是定义良好的。通常,会有更多的保证。对于c++标准库的要求,支持基本操作,如复制、赋值相同类型的对象通常就足够了。然而,用户通常希望您还可以处理所有其他方法来为对象赋值。因此,一个有用的附加保证是,您可以使用任何形式将新值“assign”给从移动对象。考虑给标准字符串s赋新值的不同方法:


s = "hello";             // assign ”hello”
a.assign(s2);            // assign the state of s2
s.clear();               // assign the empty state
std::cin >> s;           // assign the next word from standard input
std::getline(myfile, s); // assign the next line from a file

例如,下面的循环是逐行从流读入vector的常用方法:


std::string row;
while (std::getline(myStream, row))
{coll.push_back(std::move(row)); // move the line into the vector
}

另一个例子是,你可以在处理容器的通用代码中实现以下语句:


foo(std::move(obj)); // pass obj to foo()
obj.clear();         // ensure the object is empty afterwards

类似的代码可以用于释放唯一指针使用的对象的内存:


draw(std::move(up)); // the unique pointer might or might not give up ownership
up.reset();          // ensure we give up ownership and release any resource

通过声明从对象中移出的对象处于有效但未指定的状态,c++标准保证其库中的类型的任何操作都是良好定义的,只要它对所有可能的状态都能正常工作。例如,以下是根据c++标准定义的行为:


std::stack<int> stk;
...
foo(std::move(stk)); // stk gets unspecified state
stk.push(42);
...
// do something else without using stk
int i = stk.top();
assert(i == 42);     // should never fail

虽然我们不知道stk和std::move()一起传递给foo()后的值,但我们可以使用它作为一个有效的堆栈,只要我们使用它来保存值42,直到我们再次需要它。当然,提供多大程度的保证取决于您,但是要确保您的类型的用户知道这些保证。通常,他们期望您的类型提供与c++库相同的保证,这样您就可以像使用该类型的任何其他对象一样使用move-from对象,而不知道其值

Broken Invariants
c++标准库定义了所有处于“有效但未指定状态”的对象的含义如下:
对象的值是不指定的,除非满足对象的不变量,并且对象的操作行为符合其类型的规定
不变量是适用于所有可以创建的对象的保证。有了这个保证,您就可以假定从移动对象的状态意味着它的不变量没有破坏。你可以像使用非const引用形参一样使用该对象,而不需要知道所传递的实参的任何信息:你可以调用任何没有约束或前提条件的操作,并且调用的效果/结果与该类型的任何其他对象相同。例如,对于一个move -from字符串,我们可以执行所有没有前提条件的操作:
•询问它的大小
•打印出来
•遍历字符
•将其转换为C字符串
•赋新值
•追加一个字符
此外,所有这些函数仍然具有通常指定的语义:
•返回的大小可以用于安全地调用索引操作符。
•返回的大小与迭代时的字符数匹配。
•打印的字符匹配的字符序列迭代时。
•追加一个字符将会把该字符放在值的末尾。程序员可以使用这些保证(参见上面的std::stack<>示例)。然而,有时您必须显式地确保预期的不变量没有被破坏。默认的特殊move成员函数可能无法正常工作。我们将在下面的小节中讨论示例。

Restricted Invariants
不提供c++标准库对移出状态的完全保证也是有用的。也就是说,您可以有意地限制从对象移出的可能操作。例如,当对象的有效状态总是需要内存等资源时,只有部分支持的状态可能会使移动操作成本更低。在c++标准库中就有这样的情况:一些实现总是需要内存来满足基于节点的容器的所有状态(因此,即使是默认构造函数也必须分配内存)。对于这些容器,最好限制从移动对象中所能做的事情,而不是保证从移动对象中所能做的事情始终处于“有效”状态。但是,我们决定给予全额担保。理想情况下,不支持所有操作的移出状态应该是可检测的。对象应该知道这个状态,并提供一个成员函数来检查这个状态。move-from对象也可能拒绝执行在此状态下不支持的操作。然而,在一般情况下,相应的检查可能会降低性能。在c++标准库中,有些类型提供api来检查对象是否处于移出状态。例如,std::futures有一个成员函数valid(),该函数对于从移动对象返回false。但是检查从移动状态的接口是不同的。通常情况下,迁出状态是默认的构造状态,这意味着迁出状态是不变量的一部分。在任何情况下,确保您的类型的用户知道什么是定义良好的,什么是没有定义良好的

Assignable and Destructible Moved-From Objects

在大多数类中,生成的特殊move成员函数将move-from对象带入赋值操作符和析构函数可以正常工作的状态。然而,如果每个movefrom成员都是可赋值和可销毁的,赋值和从整体上销毁movefrom对象应该都能很好地工作:
•析构函数将销毁成员(未指定状态)。因为我们通常可以并且应该期望从对象中移动的成员处于有效但未指定的状态,所以生成的移动操作通常只会工作并创建正确的状态。
For example, consider the following class:


class Customer
{private:std::string name;std::vector<int> values;...
};

当customer的值被移走时,name和values都保证有一个有效的状态,以便它们的析构函数(由customer的析构函数调用)正常工作:


void foo()
{Customer c{"Michael Spencer"};... process(std::move(c));// both name and values have valid but unspecified states...
} // destructor of c will clean up name and values (whatever their state is)

另外,给c赋一个新值也是可行的,因为我们同时赋了名称和值

Non-Destructible Moved-From Objects
然而,在极少数情况下可能会出现问题。在这种情况下,通常需要实现赋值操作符或析构函数,而不仅仅是赋值或撤销成员考虑下面的类,其中使用了一个固定大小的数组,其中包含数量可变的线程对象,我们在析构函数中显式地调用join()来等待它们的结束:


#include <array>
#include <thread>
class Tasks
{private:std::array<std::thread, 10> threads; // array of threads for up to 10 tasksint numThreads{0};                   // current number of threads/tasks
public:Tasks() = default;// pass a new thread:template <typename T>void start(T op){threads[numThreads] = std::thread{std::move(op)};++numThreads;}...// at the end wait for all started threads:~Tasks(){for (int i = 0; i < numThreads; ++i){threads[i].join();}}
};

到目前为止,类不支持移动语义,因为我们有一个用户声明的析构函数。您也不能复制Tasks对象,因为复制std::线程是禁用的。但是,你可以启动多个任务并等待它们的结束:


#include "tasks.hpp"
#include <iostream>
#include <chrono>
int main()
{Tasks ts;ts.start([]{std::this_thread::sleep_for(std::chrono::seconds{2});std::cout << "\nt1 done" << std::endl; });ts.start([]{ std::cout << "\nt2 done" << std::endl; });
}

现在考虑通过生成move操作的默认实现来启用move语义


class Tasks
{private:std::array<std::thread, 10> threads; // array of threads for up to 10 tasksint numThreads{0};                   // current number of threads/tasks
public:...// OOPS: enable default move semantics:Tasks(Tasks &&) = default;Tasks &operator=(Tasks &&) = default;// at the end wait for all started threads:~Tasks(){for (int i = 0; i < numThreads; ++i){threads[i].join();}}
};

在这种情况下,您会遇到麻烦,因为默认生成的move操作可能会创建无效的Tasks状态。考虑下面的例子:


#include "tasksbug.hpp"
#include <iostream>
#include <chrono>
#include <exception>
int main()
{try{Tasks ts;ts.start([]{std::this_thread::sleep_for(std::chrono::seconds{2});std::cout << "\nt1 done" << std::endl; });ts.start([]{ std::cout << "\nt2 done" << std::endl; });// OOPS: move tasks:Tasks other{std::move(ts)};}catch (const std::exception &e){std::cerr << "EXCEPTION: " << e.what() << std::endl;}
}

在开始时,我们通过将两个任务传递给tasks对象ts来启动它们。因此,在ts中,线程数组有两个条目,numThreads为2。不幸的是,容器的move操作将移动元素,因此在std::move()之后,ts不再包含任何表示正在运行的线程的线程对象。因此,numThreads只是被复制,这意味着我们创建了一个不一致/无效的状态。析构函数最终将遍历调用join()的前两个元素,这将抛出一个异常(这是析构函数中的一个致命错误)。一般的问题是,两个成员一起定义一个有效的状态,而默认生成的move语义会产生不一致性,因此析构函数也会失败。您不能总是避免这个问题,因为当对象被销毁时,您可能总是必须显式地对对象包含的元素子集进行处理。在所有这些情况下,默认生成的移动操作可能无法工作,您应该禁用或修复它们。解决方法可能是:
•修复析构函数来处理move-from状态(在这种情况下,我们可以,例如,只调用join()如果线程对象是joinable())
•Implementing the move operations yourself
•禁用移动语义(这将是这里的行为,没有声明特殊的移动成员函数)根据零规则,你应该封装容易出错的资源管理在一个助手类型,这样应用程序程序员必须实现零特殊成员函数。在这种情况下,您可能会使用一个helper类(模板),它既提供成员(std::array和用于实际使用的元素数量的成员),又提供移动操作的正确实现。整个问题也是std::thread类设计错误的副作用。该类型不遵循RAII原则。对于所有具有运行线程的std::线程,必须在调用析构函数之前调用join()(或detach());否则,线程的析构函数将抛出。由于c++ 20,您可以并且应该使用std::jthread类,如果对象仍然表示一个运行的线程,它将自动调用join()

Dealing with Broken Invariants
不幸的是,从移动对象可以破坏“有效但未指定状态”的保证,这比破坏可销毁的要求要容易得多。我们可能会意外地将对象带入破坏其不变量的状态。幸运的是,这只是在明确请求move语义时才会出现问题,因为临时对象无论如何都会立即销毁。然而,使用std::move()标记对象可能不仅会导致显式的移动请求。你可以使用以下命令创建move-from对象:
对一个对象使用std::move()
•移动算法(std::move()和std::move_backward())
•通过将“未删除”的元素移到前面来“删除”元素的算法(例如,std::remove(), std::remove_if(), std::unique())
原则上,如果一个类的不变量被一个(生成的)Move操作破坏了,你有以下选项:
•修正了move操作,将被移出的对象带入一个不会破坏不变量的状态。
•禁用move语义。
•放宽定义所有可能的移动状态的不变量。特别是,这可能意味着必须以不同的方式实现成员函数和使用对象的函数,以处理新的可能状态。
•记录并提供一个成员函数来检查“坏掉的不变量”的状态,这样该类型的用户在使用std::move()标记后就不会再使用该类型的对象(或者只使用有限的操作集)。让我们看一些损坏的不变量的例子,并讨论如何修复它们。

Breaking Invariants Due to a Moved Value Member
move操作破坏不变量的第一个原因与对象有关,其中成员的move-from状态本身就是一个问题,因为状态是有效的,但不应该发生假设每个物件都是一张有效的Card,例如红桃8或方块k。另外,假定出于某种原因,该值是一个字符串,并且该类的不变式是每个对象都有一个表示有效Card的状态。这意味着我们可能没有默认构造函数,并且初始化构造函数会断言值是有效的。例如:


class Card
{private:std::string value; // rank + "-of-" + suit
public:Card(const std::string &v): value{v}{assertValidCard(value); // ensure the value is always valid}std::string getValue() const{return value;}
};

在这个类中,生成的特殊move成员函数创建了一个无效的状态,它打破了类的不变式,即值始终是等级后跟“-of-”再后跟花色(例如“红心皇后”)。只要我们不使用std::move()或其他移动操作,这就不是问题(该类型的析构函数对于“move -from”字符串工作得很好),但是当我们调用std::move()时,我们就会遇到麻烦。分配一个新值可以正常工作:


std::vector<Card> deck;
...                              // initialize deck
Card c{std::move(deck[0])};      // deck[0] has invalid state
deck[0] = Card{"ace-of-hearts"}; // deck[0] is valid again

然而,打印move-from卡的值可能会失败:


std::vector<Card> deck;
...                             // initialize deck
Card c{std::move(deck[0])};     // deck[0] has invalid state
print(deck[0]);                 // passing an object with broken invariant

如果print函数假设不变式没有损坏,我们可能会得到一个核心转储:


void print(const Card &c)
{std::string val{c.getValue()};auto pos = val.find("-of-"); // find position of substring (no check)std::cout << val.substr(0, pos) << ' '<< val.substr(pos + 4) << '\n'; // OOPS: possible core dump
}

这段代码在运行时可能会失败,因为对于移出的Card,它不再保证值包含“-of-”。在这种情况下,find()使用std::string::npos初始化pos,当使用pos+4作为substr()的第一个参数时,会抛出std::out_of_range类型的异常。请参阅basics/card.hpp和basics/card.cpp以获得完整的示例。
修复该类的选项如下:

  1. Disable move semantics:

class Card
{... Card(const Card &) = default;            // disable move semanticsCard &operator=(const Card &) = default; // disable move semantics
};

但是,这使得移动操作(例如,由std::sort()调用的操作)的开销更大。
2. Disable copying and moving at all:


class Card
{...Card(const Card &) = delete;            // disable copy/move semanticsCard &operator=(const Card &) = delete; // disable copy/move semantics
};

但是,你就不能再洗牌或排序了。
•Fix 损坏的特殊移动成员功能。然而,什么是有效的Fix(总是分配一个“默认值”,如“梅花a”)?如何确保具有默认值的对象在不分配内存的情况下运行良好?
•内部允许新的状态,但不允许调用getValue()或其他成员函数。您可以记录这一点(“对于从移动对象,您只允许分配一个新值。所有其他成员函数都有一个前提条件:对象不处于移动离开状态。”),甚至在成员函数内部检查这一点并引发断言或异常。
•通过引入一个Card可能没有值的新状态来扩展不变量。这意味着您必须实现移动特殊成员函数,因为您必须确保对于从移动对象,成员值处于这种状态。通常,move-from状态等价于默认构造的状态。因此,这也是提供默认构造函数的一个机会。理想情况下,您还可以提供一个成员函数来检查此状态。

通过这种更改,该类的用户必须考虑到字符串的值可能为空,并相应地更新他们的代码。例如:


void print(const Card &c)
{std::string val{c.getValue()};auto pos = val.find("-of-"); // find position of substringIf(pos != std::string::npos){ // check whether it existsstd::cout << val.substr(0, pos) << ' '<< val.substr(pos + 4) << '\n';}else{std::cout << "no value\n";}
}

或者,getValue()可以返回std::optionalstd::string(从c++ 17开始可用)。
似乎没有明显的完美解决方案。您必须考虑这些修复对于程序的较大不变量意味着什么(例如,只有一张梅花a,或者所有的卡都是有效的卡,等等),并决定使用哪一张。注意,这个类在c++ 11之前工作得很好,因为在c++ 11中不支持移动语义(这可能意味着第一个选项是最好的)。因此,c++ 11可能会为实现类时不可能实现的类引入状态。这是一种罕见的情况,但它确实意味着引入移动语义可能会破坏现有代码。有关该类的另一个示例,请参阅类Email,在这个类中,我们内部标记了移出状态,以便单独处理它,并在“移除”算法使元素处于移出状态后使这个状态可见。

Breaking Invariants Due to Moved Consistent Value Members
移动操作破坏不变量的第二个原因与两个成员必须保持一致的对象有关,但移动特殊成员函数可能会破坏这些对象。正如在线程数组的示例中所看到的,这甚至可能造成破坏析构函数的不一致。然而,更常见的情况是析构函数工作正常,但move -from状态破坏了一个不变式。考虑这样一个类,它的值有两种不同的表示形式,一个是整型值,一个是字符串值:


#include <iostream>
#include <string>
class IntString
{private:int val;          // valuestd::string sval; // cached string representation of the value
public:IntString(int i = 0): val{i}, sval{std::to_string(i)}{}void setValue(int i){val = i;sval = std::to_string(i);}... void dump() const{std::cout << " [" << val << "/'" << sval << "']\n";}
};

在这个类中,我们通常确保成员val和成员sval只是同一值的两种不同表示形式。这意味着在这个类的实现和使用中,我们通常期望其状态的int和字符串表示是一致的。然而,如果我们在这里调用move操作,我们将保留val值,但sval不再保证具有val的字符串表示形式。考虑以下程序:


#include "intstring.hpp"
#include <iostream>
int main()
{IntString is1{42};IntString is2;std::cout << "is1 and is2 before move:\n";is1.dump();is2.dump();is2 = std::move(is1);std::cout << "is1 and is2 after move:\n";is1.dump();is2.dump();
}

这个程序通常有以下输出(不要忘记从字符串变为空是典型的,但不保证):


is1 and is2 before move:
[42/’42’]
[0/’0’]
is1 and is2 after move:
[42/’’]
[42/’42’]

也就是说,自动生成的move操作破坏了两个成员总是相互匹配的不变式。这个问题有多严重?这至少是个可能的陷阱。同样,您可能会认为在一次移动之后我们不应该再使用该值(直到我们再次设置该值)。但是,程序员可能希望使用类似于c++标准库对象的策略,该策略声明对象处于有效但未指定的状态。事实是,使用相应的getter,类不再保证int和string的值匹配,这在这里可能是一个类不变式(隐式或显式声明)。您可能认为最糟糕的结果是值(现在未指定)看起来不同于您使用它的方式,但使用它没有问题,因为它仍然是一个有效的int或字符串。然而,依赖于此不变式的代码可能会被破坏。该代码可能假定字符串表示至少有一个数字。例如,如果它搜索第一个或最后一个数字,您肯定会找到一个。对于通常为空的move -from字符串,则不再是这种情况。因此,不重复检查字符串值中是否有任何字符的代码可能会遇到意外的未定义行为。同样,如何处理这个问题取决于类的设计者。然而,如果您遵循c++标准库的规则,您应该让move -from对象处于有效状态,这可能意味着您引入了一个表示“我没有任何值”的可能状态。通常,当对象的状态具有以某种方式相互依赖的成员时,必须显式地确保从移动状态处于有效状态。可能被打破的例子有:
•我们有相同值的不同表示,但其中一些被移走了。
•一个成员,如计数器,与成员中的元素数量相对应。
•布尔值声明一个字符串值被验证,但验证值被移除了。
•缓存的所有元素的平均值仍然在那里,但是值(在容器成员中)被移走了。再次注意,在不支持move语义的c++ 11之前,这个类工作得很好。当切换到c++ 11或更高版本并使用move-from对象时,不变量被破坏。

Breaking Invariants Due to Moved Pointer-Like Members
移动操作打破不变量的第三个原因与具有类似指针语义的对象(如(智能)指针)有关。考虑下面这个类的例子,其中对象使用std::shared_ptr<>来共享整数值:


class SharedInt
{private:std::shared_ptr<int> sp;
public:explicit SharedInt(int val): sp{std::make_shared<int>(val)}{}std::string asString() const{return std::to_string(*sp); // OOPS: assume there is always an int value}
};

该类的对象接收可与这些对象的副本共享的初始整数值。只要新对象只是复制,一切都很好:


SharedInt si1{42};
SharedInt si2{si1};                  // si1 and si2 share the value 42
std::cout << si1.asString() << '\n'; // OK

因为只是复制,所以SharedInt成员sp总是为它的值分配内存(通过std::make_shared<>()或通过复制一个现有的共享指针分配内存)。然而,当我们使用move语义时,如果我们仍然使用move -from对象,就会遇到未定义的行为:


SharedInt si1{42};
SharedInt si3{std::move(si1)};       // OOPS: moves away the allocated memory in si1
std::cout << si1.asString() << '\n'; // undefined behavior (probably core dump)

问题是在类内部,我们没有正确处理值可能被移走的事实,这是因为默认生成的move操作调用了共享指针的move操作,从而将所有权从原始对象移开。这意味着SharedInt的move-from状态将使成员sp处于不再拥有对象的情况,这在其成员函数asString()中无法正确处理。您可能会争辩说,为具有movefrom状态的对象调用asString()没有任何意义,因为您使用的是未指定的值,但至少标准库保证它的movefrom类型处于有效状态,因此您可以调用所有没有约束的操作。在用户定义的类型中没有提供相同的保证可能会让该类型的用户感到惊讶。从健壮编程的角度来看(避免意外、陷阱和未定义的行为),我通常建议您遵循c++标准库的规则。也就是说:移动操作不应该将对象带入破坏不变量的状态。在这种情况下,我们必须做以下其中之一:
•通过正确处理所有可能的move-from状态修复类的所有损坏操作
•禁用移动语义,以便在复制对象时没有优化
•显式实现移动操作
•调整并记录类或特定操作的不变量(约束/前提条件)(如“为从对象移动调用asString()是未定义的行为”),因为分配内存是昂贵的,在这种情况下,最好的解决方法可能是正确处理整数值的所有权可能被移走的事实。这将创建一个默认构造函数所拥有的状态,我们可以通过这个更改引入这个状态。

Fixing Broken Member Functions
第一个选项,修复所有损坏的操作,本质上意味着我们扩展类的不变量(所有对象的可能状态),以便所有操作都可以处理从移动状态。我们仍然需要做出设计决策。例如,当对一个movefrom对象(或者更一般地,一个共享指针不拥有整型值的对象)调用asString()时,我们可以:
• Still return a fallback value:


class SharedInt
{... std::string asString() const{return sp ? std::to_string(*sp) : "";}...
};

Throw an exception:


class SharedInt
{... std::string asString() const{if (!sp)throw... return std::to_string(*sp);}...
};

Force a runtime error in debug mode:


class SharedInt
{... std::string asString() const{assert(sp);return std::to_string(*sp);}...
};

Disabling Move Semantics
第二种选择是禁用移动语义,这样只能使用复制语义。我们在前面描述了如何禁用移动语义。您必须用户声明另一个特殊的成员函数。通常,=default复制特殊成员函数:


class SharedInt
{... SharedInt(const SharedInt &) = default;             // disable move semanticsSharedInt &operator=(const SharedInt &) = default;  // disable move semantics...
};

Implementing Move Semantics
第三种选择是实现move操作,这样它们就不会破坏类的不变量。为此,我们必须决定一个被移动的物体的状态应该是什么。为了支持asString()可以在不检查值是否存在的情况下调用operator*,我们必须始终提供一个值。例如,我们可以有一个静态的move-from值,我们把它赋给值被移走的对象:


#include <memory>
#include <string>
class SharedInt
{private:std::shared_ptr<int> sp;// special “value” for moved-from objects:inline static std::shared_ptr<int> movedFromValue{std::make_shared<int>(0)};
public:explicit SharedInt(int val): sp{std::make_shared<int>(val)}{}std::string asString() const{return std::to_string(*sp); // OOPS: unconditional deref}// fix moving special member functions:SharedInt(SharedInt &&si): sp{std::move(si.sp)}{si.sp = movedFromValue;}SharedInt &operator=(SharedInt &&si) noexcept{if (this != &si){sp = std::move(si.sp);si.sp = movedFromValue;}return *this;}// enable copying (deleted with user-declared move operations):SharedInt(const SharedInt &) = default;SharedInt &operator=(const SharedInt &) = default;
};

Move Semantics and noexcept
当移动语义在c++ 11中几乎完成时,我们发现了一个问题:vector重新分配不能使用移动语义。结果,引入了新的关键字noexcept。本章解释了这个问题,以及在c++代码中使用noexcept的意义

Move Constructors without noexcept
考虑下面的类,它引入了一个具有string成员的类型,并实现了一个copy构造函数和一个move构造函数,使这些构造函数的调用可见:


#include <string>
#include <iostream>
class Person
{private:std::string name;
public:Person(const char *n): name{n}{}std::string getName() const{return name;} // print out when we copy or move:Person(const Person &p): name{p.name}{std::cout << "COPY " << name << '\n';}Person(Person &&p): name{std::move(p.name)}{std::cout << "MOVE " << name << '\n';}...
};

现在让我们创建并初始化一个Person vector,并在Person存在时插入它:


#include "person.hpp"
#include <iostream>
#include <vector>
int main()
{std::vector<Person> coll{"Wolfgang Amadeus Mozart","Johann Sebastian Bach","Ludwig van Beethoven"};std::cout << "capacity: " << coll.capacity() << '\n';coll.push_back("Pjotr Iljitsch Tschaikowski");
}

程序输出如下:


COPY Wolfgang Amadeus Mozart
COPY Johann Sebastian Bach
COPY Ludwig van Beethoven
capacity: 3
MOVE Pjotr Iljitsch Tschaikowski
COPY Wolfgang Amadeus Mozart
COPY Johann Sebastian Bach
COPY Ludwig van Beethoven

我们首先将初始值复制到vector中(因为容器的std::initializer_list<>构造函数按值接受传递的参数),因此,vector通常为三个元素分配内存(在图中,我对字符串值使用了快捷方式):

重要的是接下来会发生什么:我们使用push_back()插入第四个元素,其结果如下:
因为vector内部需要更多的内存,它分配了新的内存(例如,6个元素),移动了第4个字符串(我们创建了一个临时的Person,并通过push_back()将其移动到vector中),但也将现有的元素复制到新内存中:

在这个操作的最后,vector对象销毁旧的元素,释放旧的内存,并更新其成员:
问题是,为什么vector不使用move构造函数将元素从旧内存移动到新内存?

Strong Exception Safety Guarantee
vector重新分配不使用move语义的原因是我们为push_back()提供了强大的异常处理保证:当在vector重新分配过程中抛出异常时,c++标准库保证将vector回滚到它之前的状态。也就是说,push_back()提供了一种事务保证:要么成功,要么无效。c++标准能够在c++98和c++03中提供这种保证,因为c++只能复制元素。如果在复制元素时出错,源对象仍然可用。处理异常的内部代码只是销毁到目前为止创建的副本,并释放新的内存,使vector返回到之前的状态(c++标准库假设并要求析构函数不抛出;否则,它将无法回滚)。重新分配对于移动语义来说是一个完美的地方,因为我们将元素从一个位置移动到另一个位置。因此,自c++11以来,我们希望在这里使用move语义。然而,这样我们就有麻烦了:如果在重新分配期间抛出异常,我们可能无法回滚。新内存中的元素已经窃取了旧内存中元素的值。因此,仅仅摧毁新元素是不够的;我们得把它们搬回去。但我们怎么知道把它们移回去不会失败呢?您可能认为move构造函数永远不应该抛出。这对于字符串可能是正确的(因为我们只是移动整数值和指针),但因为我们要求被移动的对象处于有效状态,这个状态可能需要内存,这意味着如果内存不足,移动可能会抛出(例如,Visual c++的基于节点的容器就是这样实现的)。我们也不能放弃这个保证,因为程序可能已经使用这个特性来避免创建矢量的备份,从而丢失可能(安全)关键的数据。而不再支持push_back()将是c++ 11接受度的噩梦。最后的决定是,只有当元素类型的move构造函数保证不抛出时,才对重新分配使用move语义。

Move Constructors with noexcept
因此,当我们保证Person类的move构造函数不会抛出时,我们的小示例程序改变了它的行为:


#include <string>
#include <iostream>
class Person
{private:std::string name;
public:Person(const char *n): name{n}{}std::string getName() const{return name;}// print out when we copy or move:Person(const Person &p): name{p.name}{std::cout << "COPY " << name << '\n';}Person(Person &&p) noexcept // guarantee not to throw: name{std::move(p.name)}{std::cout << "MOVE " << name << '\n';}...
};

如果我们现在像以前一样使用Persons:


#include "personmove.hpp"
#include <iostream>
#include <vector>
int main()
{std::vector<Person> coll{"Wolfgang Amadeus Mozart","Johann Sebastian Bach","Ludwig van Beethoven"};std::cout << "capacity: " << coll.capacity() << '\n';coll.push_back("Pjotr Iljitsch Tschaikowski");
}

我们得到如下输出:


COPY Wolfgang Amadeus Mozart
COPY Johann Sebastian Bach
COPY Ludwig van Beethoven
capacity: 3
MOVE Pjotr Iljitsch Tschaikowski
MOVE Wolfgang Amadeus Mozart
MOVE Johann Sebastian Bach
MOVE Ludwig van Beethoven

同样,我们首先将初始值复制到vector中

然而,正如输出的最后三行所显示的那样,vector现在使用move构造函数将其元素移动到新的重新分配的内存中:
这意味着在最后,vector对象只需要释放旧内存并更新其成员:

Conditional noexcept Declarations
然而,是否可以用noexcept标记移动构造函数?好的,我们移动名称(std::string)并将一些内容写入标准输出流。如果我们在那里throw,我们就违反了不throw的保证。在这种情况下,程序将在运行时调用std::terminate(),后者通常调用std::abort()来表示程序的异常结束(并通常创建一个核心转储)。因此,如果string成员和输出操作不throw,我们应该保证不throw。引入关键字noexcept是因为您可以使用它来指定不抛出的条件保证。它将如下所示(查看basics/personcond.hpp获得完整示例):


class Person
{private:std::string name;
public:... Person(Person &&p) noexcept(std::is_nothrow_move_constructible_v<std::string>&&noexcept(std::cout << name)): name{std::move(p.name)}{std::cout << "MOVE " << name << '\n';}...

使用noexcept(…),可以保证在括号内的编译时表达式为真时不会抛出异常。在这种情况下,我们需要两点来保证:
对于std::is_nothrow_move_constructible_vstd::string(在c++20之前,您必须使用std::is_nothrow_move_constructiblestd::string::value),我们使用标准类型trait(类型函数)来告诉我们std::string的move构造函数是否保证不抛出异常。
使用noexcept(std::cout << name),我们询问对该名称的输出表达式的调用是否保证不抛出异常。在这里,我们使用noexcept作为操作符,它告诉我们执行所传递表达式的所有相应操作是否保证不抛出异常
可以想象,通过这个声明,重新分配将再次使用复制构造函数。字符串的move构造函数不能保证不抛出,但输出操作符不能。然而,move构造函数通常不输出任何东西;因此,通常当成员不抛出时,我们可以保证move构造函数整体上不抛出。好消息是,如果您不自己实现move构造函数,编译器将不会检测到,除非为您提供保证。对于所有成员都保证不抛出move构造函数的类,生成的或默认的move构造函数将作为一个整体给出保证。考虑以下声明:


#include <string>
#include <iostream>
class Person
{private:std::string name;
public:Person(const char *n): name{n}{}std::string getName() const{return name;}// print out when we copy:Person(const Person &p): name{p.name}{std::cout << "COPY " << name << '\n';}// force default generated move constructor:Person(Person &&p) = default;...
};

在这种情况下,我们声明应该生成默认的move构造函数:


class Person
{...// force default generated move constructor:Person(Person &&p) = default;...
};

这意味着我们只在复制时打印。当使用生成的move构造函数时,我们只看到没有执行复制。现在让我们用我们常用的程序在最后打印出一些person:


#include "persondefault.hpp"
#include <iostream>
#include <vector>
int main()
{std::vector<Person> coll{"Wolfgang Amadeus Mozart","Johann Sebastian Bach","Ludwig van Beethoven"};std::cout << "capacity: " << coll.capacity() << '\n';coll.push_back("Pjotr Iljitsch Tschaikowski");std::cout << "name of coll[0]: " << coll[0].getName() << '\n';
}

我们得到如下输出:


COPY Wolfgang Amadeus Mozart
COPY Johann Sebastian Bach
COPY Ludwig van Beethoven
capacity: 3
name of coll[0]: Wolfgang Amadeus Mozart

我们只看到初始化列表中元素的副本。对于其他所有操作,包括重新分配,都使用默认的move构造函数。我们可以看到,在重新分配的记忆中,第一个人的名字是正确的。如果根本不指定任何成员函数,则会有相同的行为。这意味着:
•如果你实现了一个move构造函数,你应该声明它是否以及何时保证不抛出。
•如果你不需要实现move构造函数,你不需要指定任何东西。如果类的性能或该类的重新分配对象对您来说很重要,您可能还想在编译时再次检查类的move构造函数是否保证不抛出


static_assert(std::is_nothrow_move_constructible_v<Person>);

或最高到c++ 17:


static_assert(std::is_nothrow_move_constructible<Person>::value, "");

Is noexcept Worth It?
您可能想知道声明带有或多或少复杂noexcept表达式的move构造函数是否值得。Howard Hinnant用一个简单的程序演示了这种效果(在本书中略有改动):


#include <iostream>
#include <string>
#include <vector>
#include <chrono>
// string wrapper with move constructor:
struct Str
{std::string val;// ensure each string has 100 characters:Str(): val(100, 'a'){ // don’t use braces here}// enable copying:Str(const Str &) = default;// enable moving (with and without noexcept):Str(Str &&s) NOEXCEPT: val{std::move(s.val)}{}
};
int main()
{// create vector of 1 Million wrapped strings:std::vector<Str> coll;coll.resize(1000000);// measure time to reallocate memory for all elements:auto t0 = std::chrono::steady_clock::now();coll.reserve(coll.capacity() + 1);auto t1 = std::chrono::steady_clock::now();std::chrono::duration<double, std::milli> d{t1 - t0};std::cout << d.count() << "ms\n";
}

我们提供了一个类来包装长度较大的字符串(以避免小字符串优化)。注意,必须用圆括号初始化值,因为大括号会将100解释为值为100的初始字符。在类中,我们用NOEXCEPT标记move构造函数,预处理器可以用nothing或NOEXCEPT替换(例如,用-DNOEXCEPT= NOEXCEPT编译)。然后我们测量在一个vector中重新分配100万个这样的对象需要多长时间。在几乎所有平台上,使用noexcept声明move构造函数会使重新分配速度提高10倍(确保激活了显著的优化级别)。也就是说,重新分配(通常通过插入新元素强制执行)可能需要大约20毫秒,而不是200毫秒。这意味着少了180毫秒,在这180毫秒内我们不能用向量做任何事情。这是一个巨大的好处

Details of noexcept Declarations
正如所见,引入noexcept是为了允许有条件保证不抛出。通常,在编译时知道函数不能抛出可以改进代码和优化,因为您不必处理因抛出异常而可能进行的清理。注意,如果违反了noexcept保证,程序会调用std::terminate(),而后者通常会调用std::abort()来导致“程序异常终止”(例如,核心转储)。

Rules for Declaring Functions with noexcept
当声明noexcept条件时,有两个规则:
•noexcept条件必须是一个编译时表达式,该表达式产生可转换为bool类型的值。
•不能重载只有不同noexcept条件的函数。
•在类层次结构中,noexcept条件是指定接口的一部分。用一个不是noexcept的函数覆盖一个不是noexcept的基类函数是错误的(但反过来不是)。

For example:


class Base
{public:... virtual void foo(int) noexcept;virtual void foo(int); // ERROR: overload on different noexcept clause onlyvirtual void bar(int);
};
class Derived : public Base
{public:... virtual void foo(int) override;     // ERROR: override giving up the noexcept guaranteevirtual void bar(int) noexcept;     // OK (here we also guarantee not to throw)
};

然而,对于非虚函数,派生类成员可以使用不同的noexcept声明隐藏基类成员:


class Base
{public:... void foo(int) noexcept;
};
class Derived : public Base
{public:... void foo(int); // OK, hiding instead of overriding
};

条件在编译时计算后遵循相同的规则。例如,考虑下面的类层次结构:


class Base
{public:virtual void func() noexcept(sizeof(int) < 8); // might throw if sizeof(int) >= 8
};
class Derived : public Base
{public:void func() noexcept(sizeof(int) < 4) override; // might throw if sizeof(int) >= 4
};

在本例中,如果int的大小为4,则会得到编译时错误,因为基类保证在调用func()时不会抛出异常,而派生类不再为func()提供这种保证。当int的大小小于4时,两者都是noexcept,这很好。当int的大小至少为8时,两者都不是noexcept,这也是可以的。因此,派生类应该只进一步限制异常保证。

noexcept for Special Member Functions
特殊成员函数不能自动生成except条件。
noexcept for Copying and Moving Special Member Functions
在这种情况下,如果为所有基类和非静态成员调用的相应操作都保证不抛出,则该操作就保证不抛出。


#include <iostream>
#include <type_traits>
class B
{std::string s;
};
int main()
{std::cout << std::boolalpha;std::cout << std::is_nothrow_default_constructible<B>::value << '\n';std::cout << std::is_nothrow_copy_constructible<B>::value << '\n';std::cout << std::is_nothrow_move_constructible<B>::value << '\n';std::cout << std::is_nothrow_copy_assignable<B>::value << '\n';std::cout << std::is_nothrow_move_assignable<B>::value << '\n';
}

程序输出如下:


true
false
true
false
true

生成的复制构造函数和复制赋值操作符可能会引发异常,因为复制std::string可能会引发异常。但是,生成的默认构造函数、move构造函数和move赋值操作符保证不会抛出,因为std::string类的默认构造函数、move构造函数和move赋值操作符保证不会抛出注意,当用户使用=default声明这些特殊成员函数时,甚至会生成noexcept条件。因此,如果我们像下面这样声明类B,就会产生相同的效果:


class B
{std::string s;
public:B(const B &) = default;            // noexcept condition automatically generatedB(B &&) = default;                 // noexcept condition automatically generatedB &operator=(const B &) = default; // noexcept condition automatically generatedB &operator=(B &&) = default;      // noexcept condition automatically generated
};

当您有一个默认的特殊成员函数时,您可以显式地指定一个不同于生成的noexcept保证。例如


class C
{... public : C(const C &) noexcept = default; // guarantees not to throw (OK since C++20)C(C &&) noexcept(false) = default; // specifies that it might throw (OK since C++20)...
};

在c++ 20之前,如果生成的和指定的noexcept条件相矛盾,则定义的函数将被删除。
noexcept for Destructors
按照规则,析构函数在默认情况下总是保证不抛出。这既适用于生成的析构函数,也适用于实现的析构函数


class B
{std::string s;
public:... ~B(){ // automatically always declared as ~B() noexcept...}
};

使用noexcept(false),您可以在没有此保证的情况下声明它们,但这通常没有任何意义,因为c++标准库的一些保证是基于析构函数从不抛出的事实

noexcept Declarations in Class Hierarchies
我们看到,特别是当我们必须实现一个move构造函数时,我们应该声明一个noexcept保证。通常,遵循c++标准的规则,当所有基类和所有成员类型在移动赋值时都不抛出时,应该声明它不抛出。一般模式如下:


class Base
{...
};
class Drv : public Base
{MemType member;...// move constructor:Drv(Drv &&) noexcept(std::is_nothrow_move_constructible_v<Base> &&std::is_nothrow_move_constructible_v<MemType>);
};

在这里,如果基类base和成员类型MemType提供了此保证,则类Drv的move构造函数保证不会抛出。move赋值操作符可能会使用相同的模式,但注意,无论如何,应该在多态类型中删除move赋值操作符,这意味着通常不需要在派生类中实现它们。

Checking for noexcept Move Constructors in Abstract Base Classes
注意,类型特征std::is_nothrow_move_constructible<>并不总是按预期工作。对于抽象基类,它总是产生false,因为它还检查您是否可以使用move构造函数创建这种类型的对象,这对于抽象类型是不可能的。因此,在所有情况下都适用的“如果抽象基类保证不抛出,我保证不抛出”的声明不能使用标准类型特征来表述。通常,您只需(必须)知道基类move构造函数是否可能抛出异常。然而,为了让你检查每个类是否它的move构造函数保证不抛出,你可以实现以下帮助器类型特征(这里,为c++ 20实现它):


// type trait to check whether a base class guarantees not to throw
// in the move constructor (even if the constructor is not callable)
#ifndef IS_NOTHROW_MOVABLE_HPP
#define IS_NOTHROW_MOVABLE_HPP
#include <type_traits>
template <typename Base>
struct Wrapper : Base
{using Base::Base;
};
template <typename T>
static constexpr inline bool is_nothrow_movable_v = std::is_nothrow_move_constructible_v<Wrapper<T>>;
#endif // IS_NOTHROW_MOVABLE_HPP

你现在甚至可以检查抽象基类是否move构造函数是noexcept。下面的程序演示了标准类型和用户定义类型特征的不同行为:


#include "isnothrowmovable.hpp"
#include <iostream>
class Base
{std::string id;... public : virtual void print() const = 0; // pure virtual function (forces abstract base class)... virtual ~Base() = default;
protected:// protected copy and move semantics (also forces abstract base class):Base(const Base &) = default;Base(Base &&) = default;// disable assignment operator (due to the problem of slicing):Base &operator=(Base &&) = delete;Base &operator=(const Base &) = delete;
};
int main()
{std::cout << std::boolalpha;std::cout << "std::is_nothrow_move_constructible_v<Base>: "<< std::is_nothrow_move_constructible_v<Base> << '\n';std::cout << "is_nothrow_movable_v<Base>: "<< is_nothrow_movable_v<Base> << '\n';
}

程序输出如下:


std::is_nothrow_move_constructible<Base>: false
is_nothrow_movable<Base>: true

因此,如果你必须在派生自抽象基类的类中实现move构造函数,你应该使用这个助手类型特征来声明move构造函数,如下所示:


class Drv : public Base
{MemType member;...// move constructor:Drv(Drv &&) noexcept(std::is_nothrow_movable_v<Base> &&std::is_nothrow_movable_v<MemType>);
};

Value Categories
要编译表达式或语句,不仅要考虑所涉及的类型是否合适。例如,当赋值的左手边使用了整型字面值时,不能将整型赋值给整型:


int i = 42;
i = 77; // OK
77 = i; // ERROR

因此,c++程序中的每个表达式都有一个值类别。除了类型之外,值类别对于决定可以对表达式做什么也很重要。但是,在c++中值的类别随着时间的推移已经发生了变化。
History of Value Categories
从历史上看(参考Kernighan&Ritchie C, K&R C),我们只有左值和右值这两个价值类别。这些术语来自于assignments中所允许的内容:
左值可以出现在赋值的左边
根据这个定义,当你使用一个int型对象/变量时,你使用左值,但当你使用一个int型文字时,你使用右值:


int x;  // x is an lvalue when used in an expression
x = 42; // OK, because x is an lvalue and the type matches
42 = x; // ERROR: 42 is an rvalue and can be only on the right-hand side of an assignment

但是,这些类别不仅对assignments很重要。它们通常用于指定是否以及在哪里可以使用一个表达式。例如:


int x;         // x is an lvalue when used in an expression
int *p1 = &x;  // OK: & is fine for lvalues (object has a specified location)
int *p2 = &42; // ERROR: & is not allowed for rvalues (object has no specified location)

然而,在ANSI-C中,事情变得更加复杂,因为声明为const int的x不能出现在赋值的左边,但仍然可以在其他几个只有左值可以使用的地方使用:


const int c = 42;   // Is c an lvalue or rvalue?
c = 42;             // now an ERROR (so that c should no longer be an lvalue)
const int *p1 = &c; // still OK (so that c should still be an lvalue)

C语言的决定是,声明为const int的C仍然是左值,因为左值的大多数操作仍然可以被特定类型的const对象调用。唯一不能再做的事情就是在赋值的左边有一个const对象。因此,在ANSI-C中,l的含义改变为locator value. 。左值现在是一个在程序中具有指定位置的对象(例如,您可以获取地址)。同样地,右值现在可以被认为只是一个可读值。c++ 98采用了这些值类别的定义。然而,随着move语义的引入,出现了一个问题:用std::move()标记的对象应该具有哪个值类别,因为用std::move()标记的类的对象应该遵循以下规则:


std::string s;
...
std::move(s) = "hello";     // OK (behaves like an lvalue)
auto ps = &std::move(s);    // ERROR (behaves like an rvalue)

但是,请注意基本数据类型(FDTs)的行为如下:


int i;
...
std::move(i) = 42;       // ERROR
auto pi = &std::move(i); // ERROR

除了基本数据类型之外,使用std::move()标记的对象应该仍然像左值一样允许修改其值。另一方面,也有一些限制,比如您不应该获得该地址。因此,引入了一个新的类别xvalue (" expired value ")来为显式标记为不再需要此值的对象(主要是用std::move()标记的对象)指定规则。然而,前右值的大多数规则也适用于xvalue。因此,以前的主值类别右值变成了一个复合值类别,现在表示新的主值类别prvalue(以前是右值的所有值)和xvalue。有关提出这些变化的论文,请参阅http://wg21.link/n3055。

Value Categories Since C++11
自c++ 11以来,值类别如图8.1所示。

我们有以下主要类别:
• lvalue (“locator value”)
• prvalue (“pure readable value”)
• xvalue (“eXpiring value”)
复合类别如下:
•glvalue(“generalized lvalue”)作为“左值或xvalue”的通用术语
•rvalue作为“xvalue或prvalue”的常用术语

Value Categories of Basic Expressions
Examples of lvalues are:

•只有变量、函数或数据成员的名称的表达式(除了右值中的普通值成员)
•一个只是字符串文字的表达式(例如,“hello”)
函数声明返回左值引用的返回值(返回类型为type &)
•任何对函数的引用,即使使用std::move()标记(见下文)
•内置的一元*操作符的结果(即对原始指针解引用的结果)

Examples of prvalues are:
•由内置的非字符串字面量组成的表达式(例如,42,true,或nullptr)
•函数的返回类型,如果声明为value返回(return type type)
•内置的一元&操作符的结果(即,取表达式的地址会得到什么)
•lambda表达式

Examples of xvalues are:
•使用std::move()标记对象的结果
•转换为对象类型的右值引用(不是函数类型)
•函数声明返回右值引用时的返回值(返回类型为type &&)
•右值的一个非静态值成员(见下文)

For example:


class X
{};
X v;
const X c;
f(v);            // passes a modifiable lvalue
f(c);            // passes a non-modifiable lvalue
f(X());          // passes a prvalue (old syntax of creating a temporary)
f(X{});          // passes a prvalue (new syntax of creating a temporary)
f(std::move(v)); // passes an xvalue

粗略地说,作为经验法则:
•所有用作表达式的名称都是左值。
•所有用作表达式的字符串都是左值。
•所有非字符串字面值(4.2,true或nullptr)都是prvalues。
•所有没有名称的临时对象(特别是由value返回的对象)都是prvalues。
•所有带有std::move()标记的对象及其值成员都是xvalue。

值得强调的是,严格来说,glvalues、prvalues和xvalues是表达式的术语,而不是值的术语(这意味着这些术语是用词不当)。例如,变量本身不是左值;只有表示该变量为左值的表达式:


int x = 3; // here, x is a variable, not an lvalue
int y = x; // here, x is an lvalue

在第一个语句中,3是初始化变量x(而不是左值)的prvalue。在第二个语句中,x是一个左值(它的计算指定了一个包含值3的对象)。左值x被用作右值,右值用于初始化变量y。

Value Categories Since C++17
c++17有相同的值类别,但明确了值类别的语义,如图所示。现在解释价值类别的关键方法是,通常我们有两种主要的表达方式:

•glvalues:long-living对象或函数的位置表达式
•prvalues:用于初始化的短期值表达式
然后xvalue被认为是一个特殊的位置,表示一个(长期存在的)对象,其资源/值不再需要

Passing Prvalues by Value
通过这个修改,我们现在可以通过未命名的初始值来传递prvalues,即使没有有效的copy和有效的move构造函数被定义:


class C
{public:C(...);C(const C &) = delete; // this class is neither copyable ...C(C &&) = delete;      // ... nor movable
};
C createC()
{return C{...}; // Always creates a conceptual temporary prior to C++17.
} // In C++17, no temporary object is created at this point.
void takeC(C val)
{...
}
auto n = createC(); // OK since C++17 (error prior to C++17)
takeC(createC());   // OK since C++17 (error prior to C++17)

在c++ 17之前,如果没有复制或移动支持,传递createC()的创建和初始化返回值这样的prvalue是不可能的。但是,从c++ 17开始,只要不需要带有位置的对象,就可以按值传递prvalues。

Materialization
c++ 17引入了一个新术语,叫做实体化(指未命名的临时对象),此时prvalue变成了临时对象。因此,临时实体化转换是一种(通常是隐式的)prvalue到xvalue的转换
每当需要glvalue(左值或xvalue)的地方使用prvalue时,就会创建一个临时对象,并使用prvalue初始化(记住,prvalue主要是“初始化值”),然后用指定临时对象的xvalue替换prvalue。因此,在上面的例子中,严格来说,我们有:


void f(const X &p); // accepts an expression of any value category but expects a glvalue
f(X{});             // creates a temporary prvalue and passes it materialized as an xvalue

因为本例中的f()有一个引用形参,所以它需要一个glvalue参数。但是,表达式X{}是一个prvalue。因此,“临时实体化”规则开始发挥作用,表达式X{}被“转换”为一个xvalue,该xvalue指定一个用默认构造函数初始化的临时对象。注意,实体化并不意味着我们创建一个新的/不同的对象。左值引用p仍然同时绑定xvalue和prvalue,尽管后者现在总是涉及到到xvalue的转换。
Special Rules for Value Categories
Value Category of Functions
c++标准中的一个特殊规则规定,所有引用函数的表达式都是左值。例如:


void f(int)
{}
void (&fref1)(int) = f;  // fref1 is an lvalue
void (&&fref2)(int) = f; // fref2 is also an lvalue
auto &ar = std::move(f); // OK: ar is lvalue of type void(&)(int)

与对象类型不同,我们可以将非const左值引用绑定到标记为std::move()的函数,因为标记为std::move()的函数仍然是左值

Value Category of Data Members
如果使用对象的数据成员(例如,当使用std::pair<>的第一个和第二个成员时),应用特殊规则。一般情况下,数据成员的值类别如下:
•左值的数据成员是左值。
•右值的引用和静态数据成员是左值。
•纯数据成员的右值是xvalue。
该规则反映了引用或静态成员不是对象的真正组成部分。如果不再需要对象的值,这也适用于对象的纯数据成员。但是,引用其他地方或为静态的成员的值仍然可以被其他对象使用。例如:


std::pair<std::string, std::string &> foo(); // note: member second is reference
std::vector<std::string> coll;
...
coll.push_back(foo().first);  // moves because first is an xvalue here
coll.push_back(foo().second); // copies because second is an lvalue here

你需要std::move()来移动成员:


coll.push_back(std::move(foo().second)); // moves

如果你有一个左值(一个有名字的对象),你有两个选择来用std::move()标记成员:


std::move(obj).member
std::move(obj.member)

因为std::move()意味着“我不再需要这个值在这里”,看起来你应该标记obj如果你不再需要对象的值,标记成员如果你不再需要成员的值。然而,情况有点复杂
std::move() for Plain Data Members
如果成员既不是静态的也不是引用的,那么根据规则,std::move()总是将成员转换为xvalue,以便使用move语义。考虑到我们已声明如下:


std::vector<std::string> coll;
std::pair<std::string, std::string> sp;

下面的代码首先将成员移动到coll中,然后再将成员移动到coll中:


sp = ... ;
coll.push_back(std::move(sp.first)); // move string first into coll
coll.push_back(std::move(sp.second)); // move string second into coll

但是,下面的代码具有相同的效果:


sp = ... ;
coll.push_back(std::move(sp).first); // move string first into coll
coll.push_back(std::move(sp).second); // move string second into coll

在使用std::move()标记之后仍然使用obj,这看起来有点奇怪,但在这种情况下,我们知道对象的哪一部分可能会被移动,所以我们仍然可以使用不同的部分。因此,例如,当我必须实现一个move构造函数时,我更喜欢用std::move()来标记成员。

std::move() for Reference or static Members
如果成员是引用或静态的,则适用不同的规则:右值的引用或静态成员仍然是左值。同样,该规则反映了这样一个成员的值实际上不是对象的一部分。说“我不再需要对象的值”不应该意味着“我不再需要不属于对象的(成员的)值”。因此,如果你有引用成员或静态成员,你如何使用std::move()是有区别的:

对对象使用std::move()无效:


struct S
{static std::string statString; // static memberstd::string &refString;        // reference member
};
S obj;
...
coll.push_back(std::move(obj).statString); // copies statString
coll.push_back(std::move(obj).refString);  // copies refString

对成员使用std::move()具有通常的效果:


struct S
{static std::string statString;std::string &refString;
};
S obj;
...
coll.push_back(std::move(obj.statString); // moves statString
coll.push_back(std::move(obj.refString); // moves refString

这一举措是否有用则是另一个问题。窃取静态成员或引用成员的值意味着在使用的对象之外修改值。这可能是有道理的,但它也可能是令人惊讶和危险的。通常,S类型应该更好地保护对这些成员的访问。在泛型代码中,您可能不知道成员是静态的还是引用。因此,使用std::move()标记对象的方法不那么危险,尽管它看起来很奇怪:


coll.push_back(std::move(obj).mem1); // move value, copy reference/static
coll.push_back(std::move(obj).mem2); // move value, copy reference/static

以同样的方式,我们稍后将介绍的std::forward<>()可用于完美地转发对象成员。有关完整的示例,请参阅basics/members.cpp

Impact of Value Categories When Binding References
当我们将引用绑定到对象时,值类别扮演着重要的角色。例如,在c++98/ c++03中,它们定义了可以将右值赋值或传递给一个const左值引用(没有名称的临时对象或标记为std::move()的对象),但不能传递给一个非const左值引用:


std::string createString();             // forward declaration
const std::string& r1{createString()};  // OK
std::string& r2{createString()};        // ERROR

编译器在这里打印的典型错误消息是“不能将非const左值引用绑定到右值”。
调用foo2()也会得到这个错误消息:


void foo1(const std::string&);  // forward declaration
void foo2(std::string&);        // forward declaration
foo1(std::string{"hello"});     // OK
foo2(std::string{"hello"});     // ERROR

Overload Resolution with Rvalue References
让我们看看将对象传递给引用时的确切规则。假设X类中有一个非const变量v和一个const变量c:


class X
{...
};
X v{...};
const X c{...};

如果提供函数f()的所有引用重载,则绑定引用的规则表列出了将引用绑定到传入参数的形式化规则:


void f(const X &);  // read-only access
void f(X &);        // OUT parameter (usually long-living object)
void f(X &&);       // can steal value (object usually about to die)
void f(const X &&); // no clear semantic meaning

这些数字列出了重载解析的优先级,以便您可以看到在提供多个重载时调用了哪个函数。数字越小,优先级越高(优先级1表示先尝试此操作)。请注意,您只能将右值(prvalues,比如没有名称的临时对象)或xvalues(用std::move()标记的对象)传递给右值引用。这就是它们名字的由来。通常可以忽略表的最后一列,因为const右值引用在语义上没有太大意义,这意味着我们会得到以下规则:

•非const左值引用只接受非const左值。
•右值引用只接受非const右值。
•const左值引用可以接受所有内容,并在没有提供其他重载的情况下作为回退机制
下面从表中间摘录的是move语义的回退机制的规则:

如果向函数传递一个右值(临时对象或标记为std::move()的对象),并且没有特定的move语义实现(通过接受右值引用声明),则使用通常的复制语义,通过const&参数接受实参。请注意,我们将在稍后引入通用/转发引用时扩展该表。在那里我们还将了解到,有时,您可以将左值传递给右值引用(当使用模板形参时)。请注意,并不是所有带有&&的声明都遵循相同的规则。如果使用&&声明类型(或类型别名),则适用这里的规则。

Overloading by Reference and Value
可以通过引用形参和值形参声明函数:例如:


void f(X);         // call-by-value
void f(const X &); // call-by-reference
void f(X &);
void f(X &&);
void f(const X &&);

原则上,声明所有这些重载是允许的。但是,按值调用和按引用调用之间没有特定的优先级。如果有一个函数声明为按值接受参数(它可以接受任何值类别的任何参数),那么任何通过引用接受参数的匹配声明都会造成歧义。因此,您通常应该只通过值或引用(使用您认为有用的尽可能多的引用重载)接受参数,但永远不要两者都接受

When Lvalues become Rvalues
正如我们已经了解到的,当函数声明为一个具体类型的右值引用形参时,您只能将这些形参绑定到右值。例如


void rvFunc(std::string &&); // forward declaration
std::string s{...};
rvFunc(s);            // ERROR: passing an lvalue to an rvalue reference
rvFunc(std::move(s)); // OK, passing an xvalue

然而,请注意,有时传递左值似乎是可行的。例如:


void rvFunc(std::string &&); // forward declaration
rvFunc("hello");             // OK, although "hello" is an lvalue

记住,当字符串字面值用作表达式时,它是左值。因此,将它们传递给右值引用无法编译。但是,这涉及到一个隐藏操作,因为实参的类型(6个常量字符的数组)与形参的类型不匹配。我们有一个隐式类型转换,由string构造函数执行,它创建一个没有名称的临时对象。因此,我们真正所说的是:


void rvFunc(std::string &&);  // forward declaration
rvFunc(std::string{"hello"}); // OK, "hello" converted to a string is a prvalue

When Rvalues become Lvalues
现在让我们看看将形参声明为右值引用的函数的实现:


void rvFunc(std::string &&str)
{...
}

正如我们所了解的,我们只能传递右值:


std::string s{...};
rvFunc(s);                    // ERROR: passing an lvalue to an rvalue reference
rvFunc(std::move(s));         // OK, passing an xvalue
rvFunc(std::string{"hello"}); // OK, passing a prvalue

但是,当我们在函数内部使用str形参时,我们处理的是一个具有名称的对象。这意味着我们使用str作为左值。我们只能做左值允许做的事情。
这意味着我们不能直接递归调用我们自己的函数:


void rvFunc(std::string &&str)
{rvFunc(str); // ERROR: passing an lvalue to an rvalue reference
}

我们必须再次用std::move()标记str:


void rvFunc(std::string &&str)
{rvFunc(std::move(str)); // OK, passing an xvalue
}

这是我们已经讨论过的没有通过移动语义的规则的正式规范。再次注意,这是一个特性,而不是bug。如果我们传递了move语义,我们将不能使用两次带有move语义传递的对象,因为第一次使用它会失去它的值。另外,我们还需要一个临时禁用move语义的特性。如果将右值引用形参绑定到右值(prvalue或xvalue),则该对象被用作左值,我们必须再次将其转换为右值,以便将其传递给右值引用。现在,请记住,std::move()不过是将static_cast转换为右值引用。也就是说,我们在递归调用中编写的程序如下:


void rvFunc(std::string &&str)
{rvFunc(static_cast<std::string &&>(str)); // OK, passing an xvalue
}

我们将对象str强制转换为它自己的类型。到目前为止,还没有行动。但是,通过类型转换,我们还可以做其他事情:更改值类别。根据规则,当转换为右值引用时,左值会变成xvalue,因此允许我们将对象传递给右值引用。这并不是什么新鲜事:甚至在c++ 11之前,被声明为左值引用的形参在使用时也遵循左值规则。关键是声明中的引用指定了可以传递给函数的内容。对于函数内部的行为,引用是不相关的。困惑吗?这就是我们在c++标准中定义move语义和值类别规则的方式。就这样吧。幸运的是,编译器知道这些规则。如果有一件事需要你在这里学习,那就是移动语义没有被传递。如果你传递一个带有move语义的对象,你必须再次用std::move()标记它,以便将其语义转发给另一个函数。

Using decltype to Check the Type of Names
在接受右值引用形参的函数中,可以使用decltype来查询和使用形参的确切类型。只需将参数的名称传递给decltype。例如:

Using decltype to Check the Type of Names
在接受右值引用形参的函数中,可以使用decltype来查询和使用形参的确切类型。只需将参数的名称传递给decltype。例如:


void rvFunc(std::string &&str)
{std::cout << std::is_same<decltype(str), std::string>::value;    // falsestd::cout << std::is_same<decltype(str), std::string &>::value;  // falsestd::cout << std::is_same<decltype(str), std::string &&>::value; // truestd::cout << std::is_reference<decltype(str)>::value;            // truestd::cout << std::is_lvalue_reference<decltype(str)>::value;     // falsestd::cout << std::is_rvalue_reference<decltype(str)>::value;     // true
}

表达式decltype(str)总是生成str的类型,即std::string&&。只要表达式中需要该类型,就可以使用该类型。类型特征(类型函数如std::is_same<>)帮助我们处理这些类型。例如,要声明一个传入参数类型的非引用的新对象,我们可以声明:


void rvFunc(std::string &&str)
{std::remove_reference<decltype(str)>::type tmp;...
}

tmp在这个函数中具有std::string类型(我们也可以显式声明这个类型,但是如果我们将它作为T类型对象的泛型函数,那么代码仍然可以工作)。

Using decltype to Check the Value Category
到目前为止,我们只向decltype传递了名称来询问它的类型。但是,您也可以将表达式(不只是名称)传递给decltype。在这种情况下,decltype也根据以下约定生成值类别:
•对于一个prvalue,它只产生它的值类型:type
•对于lvalue,它生成其类型作为左值引用:type&
•对于xvalue,它将其类型作为右值引用:type&&


void rvFunc(std::string &&str)
{decltype(str + str)  // yields std::string because s+s is a prvaluedecltype(str[0]) // yields char& because the index operator yields an lvalue...
}

这意味着,如果您只是传递一个放在括号内的名称,它是一个表达式,而不再只是一个名称,decltype将生成它的类型和值类别。行为如下:


void rvFunc(std::string &&str)
{std::cout << std::is_same<decltype((str)), std::string>::value;    // falsestd::cout << std::is_same<decltype((str)), std::string &>::value;  // truestd::cout << std::is_same<decltype((str)), std::string &&>::value; // falsestd::cout << std::is_reference<decltype((str))>::value;            // truestd::cout << std::is_lvalue_reference<decltype((str))>::value;     // truestd::cout << std::is_rvalue_reference<decltype((str))>::value;     // false
}

将此与之前不使用附加括号的函数实现进行比较。在这里,decltype of (str)产生std::string&,因为str是std::string类型的左值。事实上,对于decltype,当我们在传递的名称周围加上额外的括号时,会产生不同的结果,当我们稍后讨论decltype(auto)时,也会产生重要的结果。
•!std::is_reference_v<decltype((expr))>检查expr是否是prvalue。
•std::is_lvalue_reference_v<decltype((expr))>检查expr是否为lvalue。
•std::is_rvalue_reference_v<decltype((expr))>检查expr是否为xvalue。
•!std::is_lvalue_reference_v<decltype((expr))>检查expr是否为rvalue。
再次注意这里使用的附加括号,以确保我们使用decltype的值类别检查形式,即使我们只传递一个名称作为expr。在c++20之前,必须跳过后缀_v,而添加::value。

Perfect Forwarding
本章介绍泛型代码中的move语义。特别地,我们讨论了通用引用(也称为转发引用)和(完全)转发。接下来的章节将讨论通用引用和完美转发的棘手细节,以及如何处理泛型代码中可能具有或不具有move语义的返回值。

What we Need to Perfectly Forward Arguments
要将带有move语义的对象转发给函数,它不仅必须绑定到右值引用;必须再次使用std::move()将其move语义转发给另一个函数。例如,记住通过引用重载的函数的主要情况的重载解析规则:


class X
{...
};
// forward declarations:
void foo(const X &); // for constant values (read-only access)
void foo(X &);       // for variable values (out parameters)
void foo(X &&);      // for values that are no longer used (move semantics)

在调用这些函数时,我们有以下规则:


X v;
const X c;
foo(v);            // calls foo(X&)
foo(c);            // calls foo(const X&)
foo(X{});          // calls foo(X&&)
foo(std::move(v)); // calls foo(X&&)
foo(std::move(c)); // calls foo(const X&)

现在假设我们想通过一个辅助函数callFoo()来间接调用foo()相同的参数。这个helper函数也需要三个重载:


void callFoo(const X &arg)
{             // arg binds to all const objectsfoo(arg); // calls foo(const X&)
}
void callFoo(X &arg)
{             // arg binds to lvaluesfoo(arg); // calls foo(X&)
}
void callFoo(X &&arg)
{                        // arg binds to rvaluesfoo(std::move(arg)); // needs std::move() to call foo(X&&)
}

在所有情况下,arg都用作左值(作为一个有名称的对象)。第一个版本将其作为const对象转发,但其他两种情况实现了两种不同的方式来转发非const实参:
•声明为左值引用的参数(绑定到没有move语义的对象)按原样传递。
•声明为右值引用的参数(绑定到具有move语义的对象)通过std::move()传递。
这允许我们完美地向前移动语义:对于任何通过移动语义传递的参数,我们保持移动语义;但是当我们得到一个没有move语义的参数时,我们不会添加move语义。只有在这个实现中才使用callFoo()调用foo()透明:


X v;
const X c;
callFoo(v);            // calls foo(X&)
callFoo(c);            // calls foo(const X&)
callFoo(X{});          // calls foo(X&&)
callFoo(std::move(v)); // calls foo(X&&)
callFoo(std::move(c)); // calls foo(const X&)

记住,传递给右值引用的右值在使用时变成了左值,这意味着我们需要std::move()将其再次作为右值传递。但是,不能到处使用std::move()。对于其他重载,使用std::move()将在传递左值时调用foo()的重载以获取右值引用。为了在泛型代码中实现完美的转发,我们总是需要对每个参数进行所有这些重载。要支持所有组合,这意味着对2个泛型参数有9个重载,对3个泛型参数有27个重载。因此,c++ 11引入了一种特殊的方法来完美地转发给定的参数,而没有任何重载,但仍然保留类型和值类别

Perfectly Forwarding const Rvalue References

尽管const右值引用没有语义意义,但如果我们想要保持用std::move()标记的常量对象的确切类型和值类别,我们甚至需要第四个重载:


void callFoo(const X &&arg)
{                        // arg binds to const rvaluesfoo(std::move(arg)); // needs std::move() to call foo(const X&&)
}

否则,调用foo(const X&)。这通常是可以的,但可能在某些情况下,我们希望保留传递了const右值引用的信息(例如,由于某种原因,提供了foo(const X&&)重载)。在这种情况下,泛型代码甚至需要对两个或三个参数进行16和64次重载,除非语言提供了用于完美转发的特殊特性。

Implementing Perfect Forwarding
为了避免对具有不同值类别的参数的函数重载,c++引入了一种特殊的机制来实现完美转发。你需要做三件事:

  1. 将调用形参作为纯右值引用(用&&声明,但不带const或volatile)。
  2. 形参的类型必须是函数的模板形参。
  3. 当将参数转发到另一个函数时,使用名为std::forward<>()的辅助函数,它在<实用程序>中声明。你必须实现一个函数,完美转发一个参数如下:

template <typename T>
void callFoo(T &&arg)
{foo(std::forward<T>(arg)); // equivalent to foo(std::move(arg)) for passed rvalues
}

std::forward<>()是一个有效的条件std::move(),因此我们得到与上面callFoo()的三次(或四次)重载相同的行为:
•如果我们传递一个右值给arg,我们有相同的效果调用foo(std::move(arg))
•如果我们传递一个左值给arg,我们有相同的效果调用foo(arg)同样的方式,我们可以完美地转发两个参数:


template <typename T1, typename T2>
void callFoo(T1 &&arg1, T2 &&arg2)
{foo(std::forward<T1>(arg1), std::forward<T2>(arg2));
}

我们还可以将std::forward<>()应用于可变数量形参的每个实参,以完美地转发它们


template <typename... Ts>
void callFoo(Ts &&...args)
{foo(std::forward<Ts>(args)...);
}

注意,我们不会对所有参数调用一次forward<>();我们分别为每个参数命名。因此,我们必须把省略号(“…”)放在forward()表达式的后面,而不是直接放在args后面。然而,这里到底发生了什么是相当棘手的,需要仔细的解释

Universal (or Forwarding) References
首先,注意我们将arg声明为右值引用形参:


template<typename T>
void callFoo(T&& arg); // arg is universal/forwarding reference

这可能会给人一种右值引用的一般规则适用的印象。然而,事实并非如此。函数模板形参的右值引用(不使用const或volatile限定)不遵循普通右值引用的规则。这是另一回事。

Two Terms: Universal and Forwarding Reference

这样的参考被称为普遍参考。不幸的是,在c++标准中还主要使用另一个术语:转发引用。这两个术语之间没有区别,只是我们这里有一个历史混乱,两个既定术语的意思是一样的。这两个术语都描述了通用/转发引用的基本方面:
•它们可以通用地绑定到所有类型的对象(const和非const)和值类别。
•它们通常用于提出论点;但请注意,这不是唯一的用法(这是我更喜欢通用参考这个术语的一个原因)。

Universal References Bind To All Value Categories

C++ 移动语义学习相关推荐

  1. 这就是神经网络 11:深度学习-语义分割-DFN、BiSeNet、ExFuse

    前言 本篇介绍三篇旷视在2018年的CVPR及ECCV上的文章.旷视做宣传做的很好,出的论文解读文章很赞,省去了我从头开始理解的痛苦,结合论文基本能很快了解全貌. 语义分割任务同时需要 Spatial ...

  2. ICCV2021|首届城市规模点云语义理解挑战赛启动了

    作者丨Qingyong Hu@知乎 来源丨https://zhuanlan.zhihu.com/p/385034191 编辑丨3D视觉工坊 Introduction 我们周围的3D世界是由各种各样的物 ...

  3. 机器人建图、感知和交互的语义研究综述

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 编辑丨当SLAM遇见小王同学 推荐一篇综述 https://arxiv.org/pdf/2101.00 ...

  4. 基于大数据与深度学习的自然语言对话

    基于大数据与深度学习的自然语言对话 发表于2015-12-04 09:44| 7989次阅读| 来源<程序员>电子刊| 5 条评论| 作者李航.吕正东.尚利峰 大数据深度学习自然语言处理自 ...

  5. Apache ServiceComb Kie | 一个语义型配置中心

    项目地址:https://github.com/apache/servicecomb-kie 这次我想分享如何管理分布式应用系统中的配置项,我们的实践路程与遇到的问题. 为什么要用配置中心 我相信大家 ...

  6. 【论文翻译】Learning from Few Samples: A Survey 小样本学习综述

    论文链接:https://arxiv.org/abs/2007.15484 摘要 Deep neural networks have been able to outperform humans in ...

  7. 自动驾驶中图像与点云融合的深度学习研究综述

    Deep Learning for Image and Point Cloud Fusion in Autonomous Driving: A Review IEEE TRANSACTIONS ON ...

  8. HTML的学习-2|HTML 标签(上)

    一.HTML 语法规范 1.1 基本语法概述 HTML 标签是由尖括号包围的关键词,例如<html>. HTML 标签通常是成对出现的,例如 <html> 和  </ht ...

  9. 学习 HTML+CSS 这一篇就够了

    文章目录 学习 HTML+CSS 这一篇就够了 ! HTML 简介 一.网页 1 .什么是网页 2. 什么是 HTML 3.网页的形成 4.网页总结 二.浏览器 1. 常用浏览器 2.浏览器内核 三. ...

  10. [HTML]HTML学习笔记

    文章目录 前言 0. 编辑器 vs code 1. HTML语法规范 1.1. 基本语法概述 1.2. 标签关系 2. HTML基本结构标签 2.1. 第一个HTML 3. vs code 3.1. ...

最新文章

  1. WinAPI: Arc - 绘制弧线
  2. 【MySQL】如何让数据库查询区分大小写
  3. 字典生成工具_CANOpen系列教程09_CANOpen对象字典
  4. 【2016年第4期】欧盟数据可携权评析
  5. C语言 const 笔记
  6. scipy.ndimage.filters.gaussian_filter()
  7. Python之旅.第八章.网络编程
  8. influxdb 统计 每天 指定时间段_抖音短视频什么时间段发布?容易上热门!
  9. 【Java并发编程一】线程安全问题
  10. 如何在C ++中使用String compare()?
  11. 用面向对象思想设计奥赛罗游戏
  12. ElasticSearch High Level REST API【3】Scroll 滚屏
  13. 菲氏微积分与Keisler微积分:两个不同时代的微积分教材
  14. java sci论文,SCI论文中那些容易被混淆的部分!你写错过吗?
  15. 互动作业Android版本下载,互动作业app
  16. 发个谷歌,百度网盘,谷歌学术可用的网站
  17. 树莓派 配置USB麦克风声卡
  18. 2022国赛正式题nfs 解题
  19. PCB做板子步骤和经验
  20. sql server 整数转换成小数,并保留小数点后两位

热门文章

  1. 美团2021届秋季校园招聘笔试真题解析:小美的跑腿代购
  2. 听课记录高中计算机,高中听课记录
  3. U盘安装ESXI 6.7,并使用U盘启动服务器
  4. Leetcode no. 347
  5. 电脑网页端远程控制手机方法
  6. PLSQL的快捷键以及使用技巧
  7. 谷歌支付 googleplay API权限相关设置
  8. 你知道Message.obtain()什么原理吗?
  9. Educational Codeforces Round 47 (Rated for Div. 2) D ---- Relatively Prime Graph
  10. win10下载ie浏览器