因此,在查找移动语义之后,我发现当您打算转移所有权时,普遍的共识是通过值传递。但在Scott Meyer's talk on Universal references中,我注意到std::vector::push_back
有2个重载:
void push_back( const T& value );
void push_back( T&& value );
所以我心想,void push_back( T value );
不够吗?我问了一些最终会导致以下测试用例的人:
#include <memory>
#include <iostream>
#include <type_traits>
struct A
{
A() { std::cout << "A Default constructor\n"; }
A(const A &) { std::cout << "A Copy\n"; }
A(A &&) { std::cout << "A Move\n"; }
};
std::aligned_storage<sizeof(A)> contents;
A& alias = *reinterpret_cast<A*>(&contents);
void ByVal(A a)
{
new (&contents) A(std::move(a));
alias.~A();
}
void ByLCRef(A const& a)
{
new (&contents) A(a);
alias.~A();
}
void ByRRef(A&& a)
{
new (&contents) A(std::move(a));
alias.~A();
}
int main()
{
A a;
std::cout << "\n";
std::cout << "ByVal(a);\n";
ByVal(a);
std::cout << "ByVal(std::move(a));\n";
ByVal(std::move(a));
std::cout << "ByVal(A());\n";
ByVal(A());
std::cout << "ByLCRef(a);\n";
ByLCRef(a);
std::cout << "ByRRef(std::move(a));\n";
ByRRef(std::move(a));
std::cout << "ByRRef(A());\n";
ByRRef(A());
}
产生以下内容:
A Default constructor
ByVal(a);
A Copy
A Move
ByVal(std::move(a));
A Move
A Move
ByVal(A());
A Default constructor
A Move
ByLCRef(a);
A Copy
ByRRef(std::move(a));
A Move
ByRRef(A());
A Default constructor
A Move
正如您所看到的,ByVal
与参考重载对相比产生了1次额外移动。所以问题是:值得吗?你什么时候创建两个重载而不是一个简单的值传递函数?
答案 0 :(得分:23)
正如你所看到的,ByVal产生了1对额外的移动 参考重载。所以问题是:值得吗?什么时候会 你创建两个重载而不是一个简单的值传递函数?
+1大多数提出此问题的人都不愿意进行分析。因此,您可以通过自己的功课来完成自己的工作。 : - )
是否值得,取决于移动构造函数的成本,以及函数占用的参数数量。在一个极端情况下,如果移动构造函数不那么快,您可能会非常关心消除它们(支持const&amp;,&amp;&amp;&amp;过载解决方案)。在另一个极端,如果你的函数有4个参数,每个参数都需要左值/右值处理,你可能不愿意写16个重载来覆盖所有情况。这需要维护很多代码,固有的代码复杂性是对bug的邀请。因此,按价值方法看起来更具吸引力(不需要重载)。
所以imho,对“值得”的问题没有一般性的回答。最好的答案是让您掌握每个解决方案成本的知识,就像您已经完成的那样,并根据具体情况做出工程判断。
<强>更新强>
在vector<T>::push_back
imho的情况下,const&amp;,&amp;&amp;过载解决方案是值得的。只有一个参数,我们不知道移动构造函数有多昂贵。实际上,我们甚至不知道 if 是否有移动构造函数。修改实验以测试后一种情况(删除移动构造函数):
ByVal(a);
A Copy
A Copy
ByLCRef(a);
A Copy
您是要支付一份还是两份,将A
复制到vector
?
即。你对参数的了解越少,你就越需要倾向于表现方面,特别是如果你正在写一些像std::vector
那样大量使用的东西。
答案 1 :(得分:5)
重要的一点是,在传递值和重载之间切换时,不需要更改客户端代码。所以它真的归结为性能与维护。由于维护通常更受青睐,我已经提出了以下经验法则:
按值传递,除非:
1.移动构造函数或移动赋值并非易事
2.物体是可复制的但不可移动的
3.您正在编写模板库,但不知道对象的类型
4.尽管对象具有琐碎的移动构造函数和赋值,但是你的探查器仍然会向你显示该程序在移动中花费了大量时间。
答案 2 :(得分:4)
想象一下,你有这个课程:
class Data {
public:
Data() { }
Data(const Data& data) { std::cout << " copy constructor\n";}
Data(Data&& data) { std::cout << " move constructor\n";}
Data& operator=(const Data& data) { std::cout << " copy assignment\n"; return *this;}
Data& operator=(Data&& data) { std::cout << " move assignment\n"; return *this;}
};
注意,一个好的C ++ 11编译器should define all these functions适合你(Visual Studio don't的一些旧版本),但我在这里为调试输出定义它们。
现在,如果你想编写一个类来存储其中一个类,我可能会像你建议的那样使用pass-by-value:
class DataStore {
Data data_;
public:
void setData(Data data) { data_ = std::move(data); }
};
我正在利用C++11 move semantics将值移动到所需的位置。然后,我可以像这样使用DataStore
:
Data d;
DataStore ds;
std::cout << "DataStore test:\n";
ds.setData(d);
std::cout << "DataStore test with rvalue:\n";
ds.setData(Data{});
Data d2;
std::cout << "DataStore test with move:\n";
ds.setData(std::move(d2));
具有以下输出:
DataStore test:
copy constructor
move assignment
DataStore test with rvalue:
move assignment
DataStore test with move:
move constructor
move assignment
哪个好。我在最后一次测试中有两个动作可能不是最佳的,但动作通常很便宜,所以我可以忍受。为了使它更加优化,我们需要重载我们稍后会做的setData
函数,但这可能是此时的过早优化。
但现在想象我们有一个可复制但不可移动的课程:
class UnmovableData {
public:
UnmovableData() { }
UnmovableData(const UnmovableData& data) { std::cout << " copy constructor\n";}
UnmovableData& operator=(const UnmovableData& data) { std::cout << " copy assignment\n"; return *this;}
};
在C ++ 11之前,所有类都是不可移动的,所以期待今天在野外找到很多类。如果我需要编写一个类来存储它,我无法利用移动语义,所以我可能会写这样的东西:
class UnmovableDataStore {
UnmovableData data_;
public:
void setData(const UnmovableData& data) { data_ = data; }
};
并通过引用传递给const。当我使用它时:
std::cout << "UnmovableDataStore test:\n";
UnmovableData umd;
UnmovableDataStore umds;
umds.setData(umd);
我得到了输出:
UnmovableDataStore test:
copy assignment
只有一个副本,如你所料。
你也可以拥有一个可移动但不可复制的课程:
class UncopyableData {
public:
UncopyableData() { }
UncopyableData(UncopyableData&& data) { std::cout << " move constructor\n";}
UncopyableData& operator=(UncopyableData&& data) { std::cout << " move assignment\n"; return *this;}
};
std::unique_ptr
是可移动但不可复制的类的示例。在这种情况下,我可能会编写一个类来存储它:
class UncopyableDataStore {
UncopyableData data_;
public:
void setData(UncopyableData&& data) { data_ = std::move(data); }
};
我经过rvalue reference并按照这样使用它:
std::cout << "UncopyableDataStore test:\n";
UncopyableData ucd;
UncopyableDataStore ucds;
ucds.setData(std::move(ucd));
使用以下输出:
UncopyableDataStore test:
move assignment
并注意我们现在只有一个好的举动。
然而,STL容器必须是通用的,它们需要处理所有类型的类并尽可能地优化。如果你真的需要上面数据存储的通用实现,它可能如下所示:
template<class D>
class GenericDataStore {
D data_;
public:
void setData(const D& data) { data_ = data; }
void setData(D&& data) { data_ = std::move(data); }
};
通过这种方式,无论我们使用的是不可复制的类还是不可复制的类,我们都能获得最佳性能,但是我们必须至少有两个setData
方法的重载,这可能会引入重复的代码。用法:
std::cout << "GenericDataStore<Data> test:\n";
Data d3;
GenericDataStore<Data> gds;
gds.setData(d3);
std::cout << "GenericDataStore<UnmovableData> test:\n";
UnmovableData umd2;
GenericDataStore<UnmovableData> gds3;
gds3.setData(umd2);
std::cout << "GenericDataStore<UncopyableData> test:\n";
UncopyableData ucd2;
GenericDataStore<UncopyableData> gds2;
gds2.setData(std::move(ucd2));
输出:
GenericDataStore<Data> test:
copy assignment
GenericDataStore<UnmovableData> test:
copy assignment
GenericDataStore<UncopyableData> test:
move assignment
Live demo.希望有所帮助。