为什么我们复制然后移动?

use*_*677 97 c++ move-semantics c++11

我看到某个地方有人决定复制一个对象并随后将其移动到一个类的数据成员的代码.这使我感到困惑,因为我认为移动的重点是避免复制.这是一个例子:

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};
Run Code Online (Sandbox Code Playgroud)

这是我的问题:

  • 为什么我们不采用右值引用str
  • 副本不会很贵,特别是给出类似的东西std::string吗?
  • 作者决定复制然后移动的原因是什么?
  • 我应该什么时候自己做?

And*_*owl 97

在我回答你的问题之前,有一件事你似乎错了:在C++ 11中按价值取得并不总是意味着复制.如果传递了一个右值,那么它将被移动(假设存在一个可行的移动构造函数)而不是被复制.并且std::string有一个移动构造函数.

与C++ 03不同,在C++ 11中,按值获取参数通常是惯用的,原因我将在下面解释.另请参阅StackOverflow上的此问答,以获取有关如何接受参数的更一般的指导原则.

为什么我们不采用右值引用str

因为那样就不可能传递左值,例如:

std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!
Run Code Online (Sandbox Code Playgroud)

如果S只有一个接受rvalues的构造函数,则上面的代码不能编译.

副本不会很贵,特别是给出类似的东西std::string吗?

如果你通过一个右值,将被移动str,并最终将被移入data.不会进行复制.另一方面,如果传递左值,则将左值复制到左侧,str然后移入左侧data.

总而言之,对于左值,两个动作为左值,一个副本和一个左手动作.

作者决定复制然后移动的原因是什么?

首先,正如我上面提到的,第一个并不总是副本; 而这说,答案是:" 因为它是有效的(std::string物品移动便宜)而且简单 ".

假设移动很便宜(这里忽略了SSO),在考虑这种设计的整体效率时,它们几乎可以被忽视.如果我们这样做,我们就有一个lvalues副本(正如我们接受左值引用时那样const)和rvalues没有副本(如果我们接受了左值引用,我们仍然会有副本const).

这意味着按值提取与提供左值const时的左值引用一样好,并且在提供右值时更好.

PS:为了提供一些背景信息,我相信这是 OP所指的问答.

  • @ user2030677:只要你需要,谁会关心副本的价格(如果你想在你的'data`成员中持有*copy*,你会这么做)?即使你将lvalue引用到`const`,你也会得到一个副本 (5认同)
  • @ user2030677:除非您存储引用,否则无法绕过该副本. (3认同)
  • @BenjaminLindley:作为初步,我写道:"*假设移动很便宜,在考虑这种设计的整体效率时,它们几乎可以被忽视.*".所以,是的,会有一个移动的开销,但这应该被认为是微不足道的,除非有证据证明这是一个真正的关注,有理由将简单的设计转变为更高效的东西. (3认同)
  • 值得一提的是它是一个C++ 11模式,它取代了`const T&`参数传递:在最坏的情况下(左值)这是相同的,但是在临时情况下你只需要移动临时值.双赢. (2认同)

Yak*_*ont 51

要理解为什么这是一个好的模式,我们应该检查C++ 03和C++ 11中的替代方案.

我们采用C++ 03方法std::string const&:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str)
  {}
};
Run Code Online (Sandbox Code Playgroud)

在这种情况下,将始终执行单个副本.如果从原始C字符串std::string构造,将构造一个,然后再次复制:两个分配.

有一个C++ 03方法来引用a std::string,然后将其交换到本地std::string:

struct S
{
  std::string data; 
  S(std::string& str)
  {
    std::swap(data, str);
  }
};
Run Code Online (Sandbox Code Playgroud)

这是"移动语义"的C++ 03版本,并且swap通常可以优化为非常便宜(非常像a move).它也应该在上下文中分析:

S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
Run Code Online (Sandbox Code Playgroud)

并强迫你形成一个非临时的std::string,然后丢弃它.(临时std::string不能绑定到非const引用).但是,只完成了一次分配.C++ 11版本需要一个&&并要求您使用std::move或使用临时调用它:这要求调用者在调用之外显式创建副本,并将该副本移动到函数或构造函数中.

struct S
{
  std::string data; 
  S(std::string&& str): data(std::move(str))
  {}
};
Run Code Online (Sandbox Code Playgroud)

使用:

S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
Run Code Online (Sandbox Code Playgroud)

接下来,我们可以做完整的C++ 11版本,它同时支持copy和move:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str) {} // lvalue const, copy
  S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
Run Code Online (Sandbox Code Playgroud)

然后我们可以检查它是如何使用的:

S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data

std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data

std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
Run Code Online (Sandbox Code Playgroud)

很明显,这种2重载技术至少与上述两种C++ 03样式一样有效,甚至更高效.我将这个2重载版本称为"最优"版本.

现在,我们将检查副本版本:

struct S2 {
  std::string data;
  S2( std::string arg ):data(std::move(x)) {}
};
Run Code Online (Sandbox Code Playgroud)

在每个场景中:

S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data

std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data

std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
Run Code Online (Sandbox Code Playgroud)

如果您将此并排与"最佳"版本进行比较,我们会做一个额外的move!我们不是一次额外的copy.

因此,如果我们假设它move很便宜,那么这个版本可以获得与最优版本几乎相同的性能,但是代码减少了2倍.

如果你说的是2到10个参数,代码的减少是指数的 - 1个参数减少2倍,2个8倍,3个16倍,4个1024x个10个参数.

现在,我们可以通过完美的转发和SFINAE来解决这个问题,允许你编写一个带有10个参数的构造函数或函数模板,SFINAE确保参数是合适的类型,然后将它们移动或复制到当地的州根据要求.虽然这可以防止程序大小问题的千倍增加,但仍然可以从该模板生成一大堆函数.(模板函数实例化生成函数)

许多生成的函数意味着更大的可执行代码大小,这本身可以降低性能.

对于几个moves 的成本,我们得到更短的代码和几乎相同的性能,并且通常更容易理解代码.

现在,这只能起作用,因为我们知道,当调用函数(在本例中是一个构造函数)时,我们将需要该参数的本地副本.我们的想法是,如果我们知道我们将要制作副本,我们应该让调用者知道我们正在通过将它放入我们的参数列表来制作副本.然后他们可以围绕这样一个事实进行优化:他们将给我们一份副本(例如,通过我们的论证).

"通过值获取"技术的另一个优点是,通常移动构造函数是noexcept.这意味着采用按值并移出其参数的函数通常可以是noexcept,将任何throws移出其体并进入调用范围(有时可以通过直接构造来避免它,或者构造项目和move参数,以控制投掷的位置).制定方法并不值得.

  • 自从我写这篇文章以来,向我指出了另一个优点:通常复制构造函数可以抛出,而移动构造函数通常是"noexcept".通过复制数据,你可以使你的函数"noexcept",并且任何复制构造都会导致**函数调用之外发生潜在的抛出(如内存不足). (5认同)

Joe*_*Joe 13

这可能是有意的,类似于复制和交换习语.基本上由于字符串是在构造函数之前复制的,因此构造函数本身是异常安全的,因为它只交换(移动)临时字符串str.


Phi*_*ßen 11

你不想通过编写移动的构造函数和复制的构造函数来重复自己:

S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}
Run Code Online (Sandbox Code Playgroud)

这是很多样板代码,特别是如果你有多个参数.您的解决方案避免了重复不必要的移动成本.(然而,移动操作应该相当便宜.)

竞争成语是使用完美转发:

template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}
Run Code Online (Sandbox Code Playgroud)

模板魔术将根据您传入的参数选择移动或复制.它基本上扩展到第一个版本,其中两个构造函数都是手动编写的.有关背景信息,请参阅Scott Meyer关于通用引用的帖子.

从性能方面来说,完美转发版本优于您的版本,因为它避免了不必要的移动.但是,有人可能会认为您的版本更易于阅读和编写.无论如何,在大多数情况下,可能的性能影响无关紧要,因此最终似乎是一种风格问题.