Используя копия & Своп идиома, мы можем легко реализовать назначение копирования с сильной исключительной безопасностью:
T& operator = (T other){
using std::swap;
swap(*this, other);
return *this;
}
Однако это требует T
быть Swappable. Какой тип автоматически std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true
благодаря std::swap
.
У меня вопрос, есть ли недостатки использования «Копировать & Переместить «идиома вместо этого?
T& operator = (T other){
*this = std::move(other);
return *this;
}
при условии, что вы реализуете перемещение-назначение для T
потому что, очевидно, в противном случае вы получите бесконечную рекурсию.
Этот вопрос отличается от Должен ли идиома «Копировать и поменять» идиома «Копировать и переместить» в C ++ 11? в том, что этот вопрос носит более общий характер и использует оператор присваивания перемещения вместо фактического перемещения членов вручную. Что позволяет избежать проблем с очисткой, которые предсказывали ответ в связанной ветке.
Способ реализации Копировать & Перемещение должно быть таким, как указал @Raxvan:
T& operator=(const T& other){
*this = T(other);
return *this;
}
но без std::move
как T(other)
уже является rvalue и clang выдаст предупреждение о пессимизации при использовании std::move
Вот.
Когда существует оператор присваивания перемещения, разница между копией & Обмен и копирование & Перемещение зависит от того, использует ли пользователь swap
метод, который имеет лучшую безопасность исключений, чем назначение перемещения. Для стандарта std::swap
безопасность исключений идентична между копией & Обмен и копирование & Переехать. Я считаю, что в большинстве случаев это будет swap
и назначение перемещения будет иметь ту же исключительную безопасность (но не всегда).
Реализация Копии & Перемещение имеет риск, когда, если оператор назначения перемещения отсутствует или имеет неправильную подпись, оператор назначения копирования будет уменьшен до бесконечной рекурсии. Однако хотя бы лязг предупреждает об этом и мимоходом -Werror=infinite-recursion
компилятору этот страх можно снять, что, откровенно говоря, мне не понятно, почему это не ошибка по умолчанию, но я отвлекся.
Я провел некоторое тестирование и много царапин на голове, и вот что я узнал:
Если у вас есть оператор присваивания перемещения, «правильный» способ сделать копию & Своп не будет работать из-за звонка operator=(T)
быть неоднозначным с operator=(T&&)
, Как указывал @Raxvan, вам нужно сделать конструкцию копирования внутри тела оператора присваивания копии. Это считается неполноценным, так как не позволяет компилятору выполнить удаление копии, когда оператор вызывается с r-значением. Однако случаи, когда бы примененная копия элизия обрабатывается заданием перемещения теперь так, что точка является спорной.
Мы должны сравнить:
T& operator=(const T& other){
using std::swap;
swap(*this, T(other));
return *this;
}
чтобы:
T& operator=(const T& other){
*this = T(other);
return *this;
}
Если пользователь не использует пользовательский swap
то шаблонное std::swap(a,b)
используется. Который по существу делает это:
template<typename T>
void swap(T& a, T& b){
T c(std::move(a));
a = std::move(b);
b = std::move(c);
}
Это означает, что исключение безопасности копирования & Своп — это та же исключительная безопасность, что и у более слабой конструкции ходов и назначений ходов. Если пользователь использует пользовательский своп, то, конечно, безопасность исключения определяется этой функцией свопинга.
В копии & Перемещение, безопасность исключения полностью определяется оператором присваивания перемещения.
Я полагаю, что рассмотрение производительности здесь является своего рода спором, поскольку оптимизация компилятора, скорее всего, не даст никакой разницы в большинстве случаев. Но я все равно отмечу, что копия и своп выполняют конструкцию копии, конструкцию перемещения и два назначения перемещения по сравнению с копией & Перемещение, которое создает копию и только одно назначение перемещения. Хотя я ожидаю, что компилятор в большинстве случаев будет запускать один и тот же машинный код, конечно, в зависимости от T.
class T {
public:
T() = default;
T(const std::string& n) : name(n) {}
T(const T& other) = default;
#if 0
// Normal Copy & Swap.
//
// Requires this to be Swappable and copy constructible.
//
// Strong exception safety if `std::is_nothrow_swappable_v<T> == true` or user provided
// swap has strong exception safety. Note that if `std::is_nothrow_move_assignable` and
// `std::is_nothrow_move_constructible` are both true, then `std::is_nothrow_swappable`
// is also true but it does not hold that if either of the above are true that T is not
// nothrow swappable as the user may have provided a specialized swap.
//
// Doesn't work in presence of a move assignment operator as T t1 = std::move(t2) becomes
// ambiguous.
T& operator=(T other) {
using std::swap;
swap(*this, other);
return *this;
}
#endif
#if 0
// Copy & Swap in presence of copy-assignment.
//
// Requries this to be Swappable and copy constructible.
//
// Same exception safety as the normal Copy & Swap.
//
// Usually considered inferor to normal Copy & Swap as the compiler now cannot perform
// copy elision when called with an rvalue. However in the presence of a move assignment
// this is moot as any rvalue will bind to the move-assignment instead.
T& operator=(const T& other) {
using std::swap;
swap(*this, T(other));
return *this;
}
#endif
#if 1
// Copy & Move
//
// Requires move-assignment to be implemented and this to be copy constructible.
//
// Exception safety, same as move assignment operator.
//
// If move assignment is not implemented, the assignment to this in the body
// will bind to this function and an infinite recursion will follow.
T& operator=(const T& other) {
// Clang emits the following if a user or default defined move operator is not present.
// > "warning: all paths through this function will call itself [-Winfinite-recursion]"// I recommend "-Werror=infinite-recursion" or "-Werror" compiler flags to turn this into an
// error.
// This assert will not protect against missing move-assignment operator.
static_assert(std::is_move_assignable<T>::value, "Must be move assignable!");
// Note that the following will cause clang to emit:
// warning: moving a temporary object prevents copy elision [-Wpessimizing-move]
// *this = std::move(T{other});
// The move doesn't do anything anyway so write it like this;
*this = T(other);
return *this;
}
#endif
#if 1
T& operator=(T&& other) {
// This will cause infinite loop if user defined swap is not defined or findable by ADL
// as the templated std::swap will use move assignment.
// using std::swap;
// swap(*this, other);
name = std::move(other.name);
return *this;
}
#endif
private:
std::string name;
};
У меня вопрос, есть ли недостатки использования «Копировать & Переместить «идиома вместо?
Да, если у вас не реализовано перемещение, вы получите переполнение стекаoperator =(T&&)
,
Если вы хотите реализовать это, вы получите ошибку компилятора (пример здесь):
struct test
{
test() = default;
test(const test &) = default;
test & operator = (test t)
{
(*this) = std::move(t);
return (*this);
}
test & operator = (test &&)
{
return (*this);
}
};
и если вы делаете test a,b; a = b;
Вы получаете ошибку:
error: ambiguous overload for 'operator=' (operand types are 'test' and 'std::remove_reference<test&>::type {aka test}')
Один из способов решить эту проблему — использовать конструктор копирования:
test & operator = (const test& t)
{
*this = std::move(test(t));
return *this;
}
Это будет работать, однако, если вы не реализуете назначение перемещения, вы можете не получить ошибку (в зависимости от настроек компилятора). Учитывая человеческую ошибку, вполне возможно, что этот случай может произойти, и вы закончите переполнение стека во время выполнения, что плохо.