Подтвердить что ты не робот

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

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

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

при условии, что вы выполняете move-assign для T, потому что, очевидно, вы в конечном итоге получаете бесконечную рекурсию.

Этот вопрос отличается от Если Idiom Copy-and-Swap станет Idiom Copy-and-Move в С++ 11?, поскольку этот вопрос более общий и использует оператор присваивания перемещения вместо фактического перемещения элементов вручную. Это позволяет избежать проблем с очисткой, которые предсказывали ответ в связанной теме.

4b9b3361

Ответ 1

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

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

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

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

Резюме

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

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

Мотивация

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

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

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

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

    Это означает, что безопасность исключений Copy and Swap - это та же самая безопасность исключений, что и слабая конструкция перемещения и назначение перемещения. Если пользователь использует собственный 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;
  };

Ответ 2

Мой вопрос в том, есть ли недостаток в использовании идиомы "Копировать и перемещать" вместо этого?

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

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