Копировать и переместить идиому?

Используя идиому Copy & Swap, мы можем легко реализовать назначение копирования с высокой безопасностью исключений:

T& operator = (T other){
    using std::swap;
    swap(*this, other);
    return *this;
}

Однако для этого требуется, чтобы T был заменяемым. Какой тип автоматически является if 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? в том, что этот вопрос носит более общий характер и использует оператор присваивания перемещения вместо фактического перемещения элементов вручную. Что позволяет избежать проблем с очисткой, которые предсказывали ответ в связанной ветке.


person Emily L.    schedule 12.04.2017    source источник
comment
Я бы также добавил к этому вопросу еще один способ сделать operator = с новым размещением: T& operator =(const T & other) { this->~T(); return * new(this) T(other); }   -  person Raxvan    schedule 12.04.2017
comment
Возможный дубликат должен идиома копирования и замены стала идиомой копирования и перемещения в C++11?   -  person Fabio says Reinstate Monica    schedule 12.04.2017
comment
Что делать, если ваш конструктор перемещения выдает исключение? Разве это не сделает копирование и перемещение идиом небезопасным?   -  person Abdus Khazi    schedule 12.04.2017
comment
@Raxvan, у которого нет сильной безопасности исключений.   -  person Emily L.    schedule 12.04.2017
comment
@FabioTurati Этот вопрос отличается от связанного тем, что этот вопрос является более общим и использует оператор присваивания перемещения вместо фактического перемещения элементов вручную. Что позволяет избежать проблем с очисткой, которые предсказывали ответ в связанной ветке.   -  person Emily L.    schedule 12.04.2017
comment
@AbdusSalamKhazi Если выбрасывает конструктор перемещения, то по умолчанию swap также выдает исключение, поскольку он использует конструктор перемещения. Специализированный swap может избежать броска, что является хорошим моментом.   -  person Emily L.    schedule 12.04.2017
comment
@Raxvan Ваш метод не является безопасным для исключений и делает недействительными все существующие ссылки на T, поскольку его время жизни заканчивается в ~T().   -  person Maxim Egorushkin    schedule 12.04.2017
comment
@ЭмилиЛ. Хорошо, извините, я отозвал дубликат флага. Чтобы другие не делали этого, я бы рекомендовал отредактировать ваш вопрос и добавить это.   -  person Fabio says Reinstate Monica    schedule 12.04.2017
comment
@Maxim Egorushkin и @Emily L. не компилируются, если вы реализуете назначение перемещения T & operator (T&& other)   -  person Raxvan    schedule 12.04.2017
comment
@HowardHinnant Хорошо известно, что копирование и обмен медленнее, чем задание копирования, написанное от руки. И причина в том, что вектор может повторно использовать емкость, если вы не выделяете новую каждый раз. Но суть вопроса не в этом.   -  person Emily L.    schedule 12.04.2017


Ответы (2)


Исправление к вопросу

Способ реализации Copy & Move должен быть таким, как указал @Raxvan:

T& operator=(const T& other){
    *this = T(other);
    return *this;
}

но без std::move, поскольку T(other) уже является значением r, и clang выдаст предупреждение о пессимизации при использовании здесь std::move.

Резюме

Когда существует оператор присваивания перемещения, разница между копированием и заменой и копированием и перемещением зависит от того, использует ли пользователь метод swap, который обеспечивает лучшую безопасность исключений, чем присваивание перемещения. Для стандартного std::swap безопасность исключений одинакова для копирования и замены и копирования и перемещения. Я считаю, что в большинстве случаев swap и назначение перемещения будут иметь одинаковую безопасность исключений (но не всегда).

Реализация копирования и перемещения сопряжена с риском, когда, если оператор присваивания перемещения отсутствует или имеет неправильную подпись, оператор присваивания копирования сведется к бесконечной рекурсии. Однако, по крайней мере, clang предупреждает об этом, и, передав -Werror=infinite-recursion компилятору, этот страх можно снять, что, честно говоря, выше моего понимания, почему это не ошибка по умолчанию, но я отвлекся.

Мотивация

Я провел несколько тестов и много почесал голову, и вот что я узнал:

  1. Если у вас есть оператор присваивания перемещения, «правильный» способ копирования и замены не будет работать из-за того, что вызов operator=(T) неоднозначен с operator=(T&&). Как указал @Raxvan, вам нужно выполнить конструкцию копирования внутри тела оператора присваивания копии. Это считается неполноценным, так как не позволяет компилятору выполнять исключение копирования, когда оператор вызывается с rvalue. Однако случаи, когда было бы применено исключение копирования, теперь обрабатываются назначением перемещения, так что этот вопрос является спорным.

  2. Мы должны сравнить:

    T& operator=(const T& other){
        using std::swap;
        swap(*this, T(other));
        return *this;
    }
    

    to:

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

    Это означает, что безопасность исключений Copy & Swap такая же безопасность исключений, как более слабая конструкция перемещения и назначение перемещения. Если пользователь использует пользовательский своп, то, конечно, безопасность исключений диктуется этой функцией свопа.

    В Copy & Move безопасность исключений полностью определяется оператором присваивания перемещения.

    Я считаю, что смотреть на производительность здесь довольно спорно, поскольку оптимизация компилятора, скорее всего, не будет иметь значения в большинстве случаев. Но я все равно замечу, что копирование и обмен выполняют построение копирования, построение перемещения и два назначения перемещения, по сравнению с копированием и перемещением, которое выполняет построение копирования и только одно назначение перемещения. Хотя я ожидаю, что компилятор в большинстве случаев выдаст один и тот же машинный код, конечно, в зависимости от 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;
  };
person Emily L.    schedule 12.04.2017

Мой вопрос: есть ли недостатки в использовании идиомы «Копировать и переместить»?

Да, вы получите переполнение стека, если не реализуете перемещение присваивания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;
}

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

person Raxvan    schedule 12.04.2017
comment
Но, конечно же, вы пишете модульные тесты, чтобы убедиться, что этого не произойдет, верно? ;) - person Emily L.; 12.04.2017
comment
Использование такого конструктора копирования предотвратит выполнение компилятором копирования-исключения :/ Кроме того, если оператор = неоднозначен в приведенном выше примере, то копирование и обмен не будут работать, верно? - person Emily L.; 12.04.2017
comment
Но опять же, копирование elision применялось бы только в том случае, если бы это было r-значение, и оно все равно обрабатывается назначением перемещения... - person Emily L.; 12.04.2017