Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should a type be move-only, just because copying may be expensive?

I have a type that is copyable, but may be expensive to copy. I have implemented the move constructor and move assignment. But I have performance issues where folks forget to call move() when passing by value.

Is it good C++11 style to remove the copy constructor, and instead provide an explicit copy() method for the rare cases when a copy is actually desired? This is idiomatic in other languages (Ruby, JavaScript) but I don't know of anything in the C++ standard library that prohibits copy purely for performance. For instance, std::vector<> is copyable, while std::unique_ptr<> and std::thread are non copyable for other reasons.

like image 240
Tim Culver Avatar asked Apr 06 '13 19:04

Tim Culver


2 Answers

Should a type be move-only, just because copying may be expensive?

No. If the semantics of your type is such that copying it is conceptually meaningful, then the correct way to make copying available is to implement a copy constructor, and give the user a chance to adopt standard syntax for invoking it:

T a;
T a = b;

If people will forget to move from objects they don't want to use anymore... Well, that's their bad:

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.

And if (for any reason) for some functions of yours it is always desirable that the caller provides an object from which it is possible to move, then let the function accept an rvalue reference:

void foo(my_class&& obj);

my_class a;
foo(a); // ERROR!
foo(std::move(a)); // OK
like image 90
Andy Prowl Avatar answered Nov 08 '22 13:11

Andy Prowl


I would treat the class as non-copyable in signature if copy is sufficiently expensive. Semantically things are copyable only if you want them to be, and an expensive copy is a decent reason to decide "no, not copyable".

The ability for something to be copied does not mean it need be implemented in a type that is copyable. The implementer of that type gets to decide if it should be semantically copyable.

I wouldn't call the operation that produced an expensive copy "copy", but rather "clone" or "duplicate".

For a way you might do this:

#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);
}

This is somewhat similar to the ways you could have written std::move like semantics prior to the existence of rvalue references, with similar downsides to such techniques, namely that the language itself has no idea what shenanigans you are up to.

It has the advantage that the syntax of the above do_copy is similar to the syntax of std::move, and it allows you to use traditional expressions without having to create trivial instances of Foo then construct a copy of another variable etc.

If the situations where we want to treat it as copyable are common (if to be avoided), I'd write a copy-wrapper around the class that knows about the duplicate method.

like image 27
Yakk - Adam Nevraumont Avatar answered Nov 08 '22 14:11

Yakk - Adam Nevraumont