我在某个地方看到了一些代码,其中有人决定复制一个对象,并随后将其移动到类的数据成员中。这让我感到困惑,因为我认为移动的整个目的是避免复制。下面是示例:
struct S
{
S(std::string str) : data(std::move(str))
{}
};
这是我的问题:
- 为什么不使用右值引用来获取
str
? - 复制会不会很昂贵,特别是对于像
std::string
这样的东西? - 作者决定先复制再移动有何原因?
- 我应该自己在什么情况下这样做?
我在某个地方看到了一些代码,其中有人决定复制一个对象,并随后将其移动到类的数据成员中。这让我感到困惑,因为我认为移动的整个目的是避免复制。下面是示例:
struct S
{
S(std::string str) : data(std::move(str))
{}
};
这是我的问题:
str
?std::string
这样的东西?std::string
确实有一个移动构造函数。为什么我们不使用rvalue引用来接受
str
?
因为这将使传递lvalue(左值)变得不可能,例如:
std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!
S
只有一个接受rvalue的构造函数,则上述代码将无法编译。即使像std::string
这样的东西,复制操作也会很昂贵吗?
str
中,最终将被移动到data
中。不会执行任何复制操作。另一方面,如果您传递lvalue,那么该lvalue将被复制到str
中,然后移动到data
中。作者为什么要决定先复制再移动呢?
首先,正如我上面提到的,第一个并不总是复制;其次,答案是:“因为它高效(std::string
对象的移动是便宜的)和简单”。const
一样),而对于rvalues则没有副本(尽管如果我们接受lvalue引用到const
仍然会有一个副本)。const
一样好,而当提供rvalues时更好。const T&
参数传递:在最坏的情况下(lvalue),这是相同的,但在临时情况下,您只需要移动临时对象。双赢。 - syamdata
的一个副本! - Andy Prowlstd::string const&
的方法:struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
在这种情况下,将始终执行单个副本。如果您从原始C字符串构造,则将构造一个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
接下来,我们可以完成完整的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
是廉价的,那么这个版本几乎可以获得与最优版本相同的性能,但代码量少了两倍。move
操作,我们可以获得更短的代码、几乎相同的性能,并且通常更容易理解的代码。throw
移出其主体并移入调用范围中(有时可以通过直接构建避免它,或构建项目并move
到参数中,以控制抛出发生的位置)。使方法成为nothrow往往是值得的。这可能是有意为之的,类似于拷贝并交换惯用语法。基本上,由于在构造函数之前复制了字符串,因此构造函数本身是异常安全的,因为它只交换(移动)临时字符串str。
您不想重复编写移动构造函数和复制构造函数:
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 Meyer在通用引用上发布的文章。
从性能方面来看,完美转发版本优于您的版本,因为它避免了不必要的移动。但是,人们可以认为您的版本更易于阅读和编写。在大多数情况下,可能不会有太大的性能影响,因此这似乎最终是一种风格问题。