Реализация назначения переноса в терминах деструктора и перемещения конструктора

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

Рассмотрим следующую реализацию оператора move-assign:

Class& operator=(Class&& rhs)
{
    this->~Class();                    // call destructor
    new (this) Class(std::move(rhs));  // call move constructor in-place
}
  • Является ли действительным реализовать оператор переадресации таким образом? То есть, вызывает ли вызов деструктора и конструктора таким образом не запускать какие-либо правила жизни объекта на языке?

  • Это хорошая идея, чтобы реализовать оператор переадресации таким образом? Если нет, то почему нет, и есть ли лучший канонический способ?

Ответ 1

Недействительно: что, если это назначение переноса называется частью перемещения дочернего объекта? Затем вы уничтожаете дочерний элемент (при условии, что он имеет виртуальный деструктор) и воссоздаете на своем месте родительский объект.

Я бы сказал, что даже в не виртуальном контексте это все еще плохая идея, потому что вы не видите синтаксис очень часто, и это может сделать код более сложным для будущих помощников.

Лучший подход заключается в том, чтобы избежать необходимости полностью писать свой собственный конструктор движений (и использовать значение по умолчанию), если все члены вашего класса позаботятся о переходе. Например, полагайтесь на unique_ptr и т.д. В противном случае кажется, что его реализация в терминах swap (как копирование и смена для назначения копии) будет легко понятным механизмом.

Ответ 2

  • Он может быть действительным (1). Чтобы решить вашу конкретную проблему о времени жизни dtor/ctor, да, это действительно (2). Именно так работала исходная реализация для вектора.
  • Это может быть хорошая идея (возможно, это не так), но вы можете не захотеть канонического способа. (3)

(1) Существует споры о том, должны ли быть или должны быть действительны в случае с самодвижением.   Рассуждение о безопасности самообслуживания - это позиция, в которой код должен быть безопасным (duh), мы, конечно, ожидаем, что самоопределение будет безопасным. Также некоторые отчеты о работе пользователей, которые для многих алгоритмов, которые используют перемещение, самодвижущиеся, возможны и утомительны для проверки.

Аргументирование безопасности самодвижения - это позиция, согласно которой вся точка семантики перемещения - это экономия времени, и поэтому ходы должны быть как можно быстрее. Проверка самодвижения может быть дорогостоящей по сравнению с затратами на ход. Обратите внимание, что компилятор никогда не будет генерировать код для самостоятельного перемещения, потому что естественные (не литые) значения не могут быть самодвижущимися. Единственный способ иметь самостоятельное перемещение - это принудительное выключение "std:: move()". Это ставит нагрузку на вызывающего устройства std:: move(), чтобы убедиться, что самоперемещение не задействовано или не убеждает себя, что это не так. Обратите также внимание на то, что было бы тривиально создать пользовательский эквивалент "std:: move", который проверял на самодвижение, а затем ничего не делал. Если вы не поддерживаете самодвижение, вы можете документировать это.

(2) Это не шаблон, поэтому вы можете узнать, выражается ли выражение "новый (этот) класс (std:: move (rhs)"; может бросить. Если это возможно, то нет, это недействительно.

(3) Этот код может быть загадкой для сопровождающих, которые могут ожидать более традиционный подход подкачки, но есть потенциальные недостатки в подходе к обмену. Если ресурсы, выпущенные мишенью, должны быть выпущены как можно скорее (например, мьютекс), тогда у swap есть недостаток, что ресурсы заменяются на объект источника перемещения. Если перемещение является результатом вызова "std:: move()", объект исходного объекта перемещения может быть немедленно удален. (Поскольку это не шаблон, вы можете узнать, какие ресурсы освобождаются. Если память является единственным освобождаемым ресурсом, это не проблема.)

Лучший подход может заключаться в том, чтобы разделить код освобождения ресурсов от деструктора и код перемещения ресурсов от конструктора перемещения, а затем просто вызвать эти (встроенные) подпрограммы в этом операторе присваивания перемещения.