我在某处看到的代码中有人决定复制一个对象,然后将其移动到类的数据成员中。这让我感到困惑,因为我认为移动的全部目的是避免复制。这是示例:
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
吗?Answers:
在我回答您的问题之前,您似乎错了一件事情:在C ++ 11中按价值取值并不总是意味着复制。如果传递了一个右值,它将被移动(假设存在可行的move构造函数),而不是被复制。并std::string
具有移动构造函数。
与C ++ 03不同,在C ++ 11中,按值获取参数通常是惯用的,原因是我将在下面解释。另请参阅有关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),在考虑此设计的整体效率时,实际上可以忽略它们。如果这样做,我们将为左值创建一个副本(就像我们接受对的左值引用时那样const
),而没有右值的副本(而如果接受对的左值引用,我们仍然会有一个副本const
)。
这意味着const
在提供左值时,按值取值与通过左值进行取值一样好,而在提供右值时则更好。
const T&
参数传递:在最坏的情况下(左值),这是相同的,但是在临时情况下,您只需要移动临时情况即可。双赢。
data
会员中,那么会花多长时间)?即使您按左值引用const
data
!
为了理解为什么这是一个好的模式,我们应该研究C ++ 03和C ++ 11中的替代方法。
我们有采用C ++ 03的方法std::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
在这种情况下,将始终执行单个副本。如果从原始C字符串std::string
构造,将构造a,然后再次复制:两个分配。
有一种C ++ 03方法,它引用a std::string
,然后将其交换为local 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
不能绑定到非常量引用)。但是,仅完成一次分配。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版本,同时支持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
};
然后,我们可以检查其用法:
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减少8倍,4个1024减少10个参数。
现在,我们可以通过完善的转发和SFINAE来解决此问题,允许您编写一个带有10个参数的构造函数或函数模板,执行SFINAE以确保参数为适当的类型,然后将它们移动或复制到所需的本地状态。虽然这可以防止程序大小增加一千倍,但是仍然可以从该模板生成大量函数。(模板函数实例化生成函数)
而且,大量生成的函数意味着更大的可执行代码大小,这本身可能会降低性能。
花费几move
秒钟的时间,我们就能获得较短的代码和几乎相同的性能,并且通常更易于理解。
现在,这仅起作用,因为我们知道在调用函数(在这种情况下为构造函数)时,我们将需要该参数的本地副本。这样的想法是,如果我们知道要复制,则应将其放入参数列表中,从而使调用者知道我们正在复制。然后,他们可以围绕将要给我们副本的事实进行优化(例如,进入我们的论点)。
“按价值获取”技术的另一个优点是,通常将构造函数移动到其他对象,这意味着按值获取并移出其参数的函数通常也可以是其他对象,将任何throw
s移出其主体并移入调用范围(谁有时可以通过直接构造来避免这种情况,或者可以构造项目并将其move
放入参数中以控制发生抛出的位置。)使方法不抛出异常通常是值得的。
noexcept
。通过按拷贝获取数据,您可以使您的函数生效noexcept
,并使任何导致潜在抛出(例如内存不足)的副本构造都发生在函数调用之外。
您不想通过为移动编写一个构造函数而为副本编写一个构造函数来重复自己:
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的通用参考文章。
从性能方面来说,完美的转发版本要比您的版本优越,因为它避免了不必要的移动。但是,有人会说您的版本更容易读写。无论如何,可能的性能影响在大多数情况下都无关紧要,因此最终似乎与样式有关。