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

时间:2013-05-23 22:03:23

标签: c++ c++11 move-semantics

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

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};

以下是我的问题:

  • 为什么我们不对str进行右值引用?
  • 副本是不是很昂贵,特别是std::string
  • 作者决定复制然后移动的原因是什么?
  • 我应该在什么时候自己做?

4 个答案:

答案 0 :(得分:95)

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

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

  

为什么我们不对str进行右值引用?

因为这样就无法传递左值,例如:

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

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

  

副本不会很昂贵,特别是std::string之类的内容吗?

如果您传递右值,那么已移动str,并最终将其移至data。不会进行复制。另一方面,如果您传递左值,则左值将复制str,然后移至data

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

  

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

首先,正如我上面提到的,第一个并不总是副本;这说,答案是:“因为它是有效的(std::string对象的移动便宜)和简单的”。

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

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

P.S。:为了提供一些背景信息,我相信OP指的是this is the Q&A

答案 1 :(得分:51)

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

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

struct S
{
  std::string data; 
  S(std::string const& str) : data(str)
  {}
};

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

有一种C ++ 03方法可以引用std::string,然后将其交换到本地std::string

struct S
{
  std::string data; 
  S(std::string& str)
  {
    std::swap(data, str);
  }
};

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

S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal

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

struct S
{
  std::string data; 
  S(std::string&& str): data(std::move(str))
  {}
};

使用:

S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal

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

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
};

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

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

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

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

struct S2 {
  std::string data;
  S2( std::string arg ):data(std::move(x)) {}
};

在每个场景中:

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

如果将此并排与“最佳”版本进行比较,我们只会再增加一个move!我们不会再做一次额外的copy

因此,如果我们假设move便宜,那么这个版本的性能几乎与最优版本相同,但代码却少了2倍。

如果你说的是2到10个参数,代码的减少是指数的 - 1个参数减少2倍,2个变换4倍,3个带16倍,4个带1024个10倍。

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

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

对于少数move的费用,我们可以获得更短的代码和几乎相同的性能,并且通常更容易理解代码。

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

'take by value'技术的另一个优点是,通常移动构造函数是noexcept。这意味着采用by-value并移出其参数的函数通常可以是noexcept,将任何throw移出他们的身体和调用范围(有时可以通过直接构造来避免它,或者构造项目和move到参数中,以控制投掷发生的地方)。制定方法不值得。

答案 2 :(得分:13)

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

答案 3 :(得分:11)

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

S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}

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

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

template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}

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

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