传递值与const&和&&重载

Lyb*_*rta 37 c++ rvalue-reference move-semantics c++11

因此,在查找移动语义之后,我发现当您打算转移所有权时,普遍的共识是通过值传递.但在Scott Meyer关于Universal引用的讨论中,我注意到std::vector::push_back有2个重载:

void push_back( const T& value );
void push_back( T&& value );
Run Code Online (Sandbox Code Playgroud)

所以我心想,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());
}
Run Code Online (Sandbox Code Playgroud)

产生以下内容:

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
Run Code Online (Sandbox Code Playgroud)

如您所见,ByVal与参考过载对相比,产生了1次额外移动.所以问题是:值得吗?你何时会创建两个重载而不是一个简单的值传递函数?

How*_*ant 23

正如您所看到的,与参考重载对相比,ByVal产生了1个额外的移动.所以问题是:值得吗?你何时会创建两个重载而不是一个简单的值传递函数?

+1大多数提出此问题的人都不愿意进行分析.因此,您可以通过自己的功课来完成自己的工作.:-)

是否值得,取决于移动构造函数的成本,以及函数所需的参数数量.在一个极端情况下,如果移动构造函数不是那么快,你可能会非常关心消除它们(支持const&,&&重载解决方案).在另一个极端,如果你的函数有4个参数,每个参数需要左值/右值处理,你可能不愿意写16个重载来覆盖所有情况.这是需要维护的大量代码,固有的代码复杂性是对bug的邀请.因此,按价值方法看起来更具吸引力(不需要重载).

因此,对于"值得"的问题,没有一般的答案.最好的答案是让您掌握每个解决方案成本的知识,就像您已经完成的那样,并根据具体情况做出工程判断.

更新

vector<T>::push_backimho 的情况下,const&,&&重载解决方案是值得的.只有一个参数,我们不知道移动构造函数有多昂贵.实际上,我们甚至不知道是否有移动构造函数.修改您的实验以测试后一种情况(删除移动构造函数):

ByVal(a);
A Copy
A Copy

ByLCRef(a);
A Copy
Run Code Online (Sandbox Code Playgroud)

你想付一两个副本来复制你Avector吗?

也就是说,你对参数的了解越少,你就越需要倾向于表现方面,特别是如果你写的东西用得那么多std::vector.


Lyb*_*rta 5

重要的一点是,在传递值和重载之间切换时,不需要更改客户端代码.所以它真的归结为性能与维护.而且由于维护通常更受青睐,我提出了以下经验法则:

除非通过值:
1.移动构造函数或移动赋值并非易事.
2.该物体是可复制的但不可移动的.
3.您正在编写模板库,但不知道对象的类型.
4.尽管对象具有琐碎的移动构造函数和赋值,但是您的探查器仍然向您显示该程序在移动中花费了大量时间.


Chr*_*rew 5

存储可移动和可复制的类

想象一下你有这个类:

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;}  
};
Run Code Online (Sandbox Code Playgroud)

注意,一个好的 C++11 编译器应该为你定义所有这些函数(一些旧版本的Visual Studio 没有),但我在这里定义它们是为了调试输出。

现在,如果您想编写一个类来存储这些类之一,我可能会像您建议的那样使用按值传递:

class DataStore {
  Data data_;
 public: 
  void setData(Data data) { data_ = std::move(data); }
};
Run Code Online (Sandbox Code Playgroud)

我正在利用C++11 移动语义将值移动到所需位置。然后我可以这样使用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));
Run Code Online (Sandbox Code Playgroud)

其中有以下输出:

DataStore test:
  copy constructor
  move assignment
DataStore test with rvalue:
  move assignment
DataStore test with move:
  move constructor
  move assignment
Run Code Online (Sandbox Code Playgroud)

这很好。我在最后一次测试中有两个动作,这可能不是最佳的,但动作通常很便宜,所以我可以接受。为了使它更优化,我们需要重载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;}  
};
Run Code Online (Sandbox Code Playgroud)

在 C++11 之前,所有类都是不可移动的,所以预计今天会在野外找到很多类。如果我需要编写一个类来存储它,我无法利用移动语义,所以我可能会写这样的东西:

class UnmovableDataStore {
  UnmovableData data_;
 public:
  void setData(const UnmovableData& data) { data_ = data; }
};
Run Code Online (Sandbox Code Playgroud)

并通过引用到const。当我使用它时:

  std::cout << "UnmovableDataStore test:\n";
  UnmovableData umd;
  UnmovableDataStore umds;
  umds.setData(umd);
Run Code Online (Sandbox Code Playgroud)

我得到输出:

UnmovableDataStore test:
  copy assignment
Run Code Online (Sandbox Code Playgroud)

如您所料,只有一份副本。

存储不可复制的类

你也可以有一个可移动但不可复制的类:

class UncopyableData {
 public:
  UncopyableData() { } 
  UncopyableData(UncopyableData&& data) { std::cout << "  move constructor\n";}
  UncopyableData& operator=(UncopyableData&& data) { std::cout << "  move assignment\n"; return *this;}    
};
Run Code Online (Sandbox Code Playgroud)

std::unique_ptr是一个可移动但不可复制的类的例子。在这种情况下,我可能会编写一个类来存储它,如下所示:

class UncopyableDataStore {
  UncopyableData data_;
 public:
  void setData(UncopyableData&& data) { data_ = std::move(data); }
};
Run Code Online (Sandbox Code Playgroud)

我通过右值引用并像这样使用它:

  std::cout << "UncopyableDataStore test:\n";
  UncopyableData ucd;
  UncopyableDataStore ucds;
  ucds.setData(std::move(ucd));
Run Code Online (Sandbox Code Playgroud)

具有以下输出:

UncopyableDataStore test:
  move assignment
Run Code Online (Sandbox Code Playgroud)

并注意我们现在只有一个好的移动。

通用容器

然而,STL 容器需要是通用的,它们需要与所有类型的类一起工作并尽可能优化。如果您真的需要上述数据存储的通用实现,它可能如下所示:

template<class D>
class GenericDataStore {
  D data_;
 public:
  void setData(const D& data) { data_ = data; }
  void setData(D&& data) { data_ = std::move(data); }   
};
Run Code Online (Sandbox Code Playgroud)

通过这种方式,无论我们使用的是不可复制的还是不可移动的类,我们都能获得最佳性能,但我们必须至少有两个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));
Run Code Online (Sandbox Code Playgroud)

输出:

GenericDataStore<Data> test:
  copy assignment
GenericDataStore<UnmovableData> test:
  copy assignment
GenericDataStore<UncopyableData> test:
  move assignment
Run Code Online (Sandbox Code Playgroud)

现场演示。希望有帮助。