Должен ли тип быть перемещенным только потому, что копирование может быть дорогостоящим?

У меня есть тип, который можно копировать, но может быть дорогостоящим для копирования. Я реализовал конструктор перемещения и переместил назначение. Но у меня проблемы с производительностью, когда люди забывают вызывать move() при передаче по значению.

Хорошо ли стиль С++ 11 удалить конструктор копирования и вместо этого предоставить явный метод copy() для редких случаев, когда действительно требуется копия? Это идиоматично на других языках (Ruby, JavaScript), но я ничего не знаю в стандартной библиотеке С++, которая запрещает копирование исключительно для производительности. Например, std::vector < > является скопируемым, тогда как std:: unique_ptr < > и std:: thread не могут быть скопированы по другим причинам.

Ответ 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.

И если (по какой-либо причине) для некоторых ваших функций всегда желательно, чтобы вызывающий объект предоставил объект, из которого можно перемещаться, а затем пусть функция принимает ссылку rvalue:

void foo(my_class&& obj);

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

Ответ 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 как семантику до появления ссылок rvalue с аналогичными недостатками таких методов, а именно, что сам язык не имеет представления о том, что вы делаете с помощью shhenanigans.

Преимущество состоит в том, что синтаксис вышеуказанного do_copy аналогичен синтаксису std::move, и он позволяет использовать традиционные выражения без необходимости создавать тривиальные экземпляры Foo, а затем создавать копию другого переменная и т.д.

Если ситуации, когда мы хотим рассматривать их как скопируемые, являются общими (если их следует избегать), я бы написал оболочку-копию вокруг класса, который знает о методе duplicate.

Ответ 3

Нет. Если тип скопирован, то тип может быть скопирован. Это означает, что его конструктор копирования доступен и работает. Это не означает, что есть какая-то функция-член, имя которой выглядит последовательно в символах c, o, p и y, что делает "похоже на почти аналогичную вещь".