【问题标题】:Should a type be move-only, just because copying may be expensive?一个类型是否应该是只移动的,仅仅因为复制可能很昂贵?
【发布时间】:2013-03-29 02:56:35
【问题描述】:

我有一个可复制的类型,但复制起来可能很昂贵。我已经实现了移动构造函数和移动赋值。但是我有性能问题,人们在传递值时忘记调用 move()。

删除复制构造函数,而是为实际需要复制的极少数情况提供显式的 copy() 方法是不是很好的 C++11 风格?这在其他语言(Ruby、JavaScript)中是惯用的,但我不知道 C++ 标准库中有任何内容纯粹为了性能而禁止复制。例如,std::vector 是可复制的,而 std::unique_ptr 和 std::thread 由于其他原因是不可复制的。

【问题讨论】:

  • 我想copy 成员函数的问题在于它破坏了所有使用赋值运算符的模板代码。

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


【解决方案1】:

一个类型应该是只能移动的,因为复制可能很昂贵吗?

。如果你的类型的语义使得复制它在概念上有意义,那么使复制可用的正确方法是实现一个复制构造函数,并让用户有机会采用标准语法来调用它:

T a;
T a = b;

如果人们忘记从他们不想再使用的物体上移开......好吧,那是他们的坏事:

T c = std::move(a); // I'm doing it right (if I no longer need object a);
T d = b; // If I don't need b anymore, I'm doing it wrong.

如果(出于任何原因)你的某些函数总是希望调用者提供一个可以从中移动的对象,那么让函数接受一个右值引用:

void foo(my_class&& obj);

my_class a;
foo(a); // ERROR!
foo(std::move(a)); // OK

【讨论】:

  • 在某些情况下,可复制类型不应通过复制构造来复制:virtual T* Clone() const = 0 用于接口类(或非最终实现)表示可复制性,但不能作为复制构造函数实现。现在,value_ptr<T> 包装器可以让您回到复制构造的世界,但 T 类本身是可复制的,但没有复制构造函数(至少不是公共的)。
  • @Yakk:我不同意。任何标准容器的复制都可能非常昂贵,但它是可复制的。个人认为,“复制”的操作有语言定义的语义和语言定义的语法。这两件事应该匹配。
  • 我认为您的意思是评论我的回答,而不是我的评论。 :)
  • 在任何情况下,标准容器都有优先级:虽然前向列表有大小,并且可以实现size(),但它很昂贵。所以标准省略了size(),想知道大小就得自己算了。
  • @TimCulver:当然不可复制类型有存在的理由,如果某些东西不需要复制,那么它不应该执行它(我也喜欢移动语义使容器更灵活) .我的观点是,如果某些东西需要复制的语义,那应该通过复制的语法来提供。
【解决方案2】:

如果副本足够昂贵,我会将类视为不可复制的签名。从语义上讲,只有您希望它们是可复制的,而昂贵的副本是决定“不,不可复制”的正当理由。

可复制的能力并不意味着它需要以可复制的类型实现。该类型的实现者可以决定它是否应该在语义上是可复制的。

我不会将产生昂贵副本的操作称为“复制”,而是将其称为“克隆”或“复制”。

你可以这样做:

#include <utility>

template<typename T>
struct DoCopy {
  T const& t;
  DoCopy( T const& t_ ):t(t_) {}
};
template<typename T>
DoCopy<T> do_copy( T const& t ) {
  return t;
}
struct Foo {
  struct ExpensiveToCopy {
    int _[100000000];
  };
  ExpensiveToCopy* data;
  Foo():data(new ExpensiveToCopy()) {}
  ~Foo(){ delete data; }
  Foo(Foo&& o):data(o.data) { o.data = nullptr; }
  Foo& operator=(Foo&& o) { data=o.data; o.data=nullptr; return *this; }
  Foo& operator=(DoCopy<Foo> o) {
    delete data;
    if (o.t.data) {
      data=new ExpensiveToCopy(*o.t.data);
    } else {
      data=new ExpensiveToCopy();
    }
    return *this;
  }
  Foo( DoCopy<Foo> cp ):data(cp.t.data?new ExpensiveToCopy( *cp.t.data ):new ExpensiveToCopy() ) {};
};
int main() {
  Foo one;
  // Foo two = one; // illegal
  Foo three = std::move(one); // legal
  Foo four;
  Foo five = do_copy(three);
  four = std::move(three);
  five = do_copy(four);
}

这有点类似于在右值引用存在之前编写std::move 之类的语义的方式,与此类技术有类似的缺点,即语言本身不知道你在做什么恶作剧。

它的优点是上面do_copy的语法类似于std::move的语法,它允许你使用传统的表达式而不必创建Foo的简单实例然后构造另一个的副本变量等。

如果我们希望将其视为可复制的情况很常见(如果要避免的话),我会围绕知道duplicate 方法的类编写一个复制包装器。

【讨论】:

  • 我会让图书馆的用户成为评委。
  • 我喜欢这个解决方案。我为可复制的层次结构做了类似的事情,类似移动的语法很不错。
【解决方案3】:

没有。如果类型是可复制的,则该类型是可复制的。这意味着它的复制构造函数可用并且工作。这并不意味着有一些成员函数的名称依次类似于字符copy,这“几乎是类似的事情”。

【讨论】:

  • 因此,如果他们禁用复制构造,则该类型不可复制。它是可复制的、可克隆的或其他一些其他同义动词——但显然不可复制。这是怎么回事?
  • @Yakk:我同意你的分类。这是个问题吗?嗯,谁知道呢?取决于您的类型想要。但是你当然不能再称它为可复制了,而且就许多其他代码片段(例如容器)而言,你的类型永远不会被复制,因为你已经下令这样做了。
  • 对,我不反对分析。我建议让我的类型不可复制,尽管它显然是一个值类型。
猜你喜欢
  • 1970-01-01
  • 2011-09-27
  • 1970-01-01
  • 1970-01-01
  • 2020-12-17
  • 2016-10-28
  • 2017-10-12
  • 2012-03-23
  • 1970-01-01
相关资源
最近更新 更多