c++ - 관용구 복사 및 이동?




c++11 move-semantics (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; 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;
}

이 작업은 가능하지만 이동 지정을 구현하지 않으면 컴파일러 설정에 따라 오류가 발생하지 않을 수 있습니다. 인간의 실수를 고려할 때,이 경우가 발생할 수 있고 런타임에 스택 오버플로가 발생할 가능성이 있습니다.

카피 & 스왑 (copy & swap) 관용구를 사용함으로써 우리는 강력한 예외 안전성을 가진 복사 할당을 쉽게 구현할 수 있습니다 :

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

그러나 이것은 TSwappable 것을 요구합니다. std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true std::swap 덕택에 자동으로 어떤 타입이 std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true ?

내 질문은, 거기에 "복사 및 이동"관용구를 사용하여 단점이 있습니까? 이렇게 :

T& operator = (T other){
    *this = std::move(other);
    return *this;
}

T 대한 T 지정을 구현했다면, 분명히 무한 재귀로 끝나기 때문입니다.

이 질문은 Copy-and-Swap Idiom이 C ++ 11의 Copy-and-Move Idiom이되어야하는 것과는 다른 것 입니까? 이 질문은보다 일반적이며 수동으로 멤버를 실제로 이동하는 대신 이동 할당 연산자를 사용한다는 점에서 다릅니다. 따라서 연결된 스레드에서 응답을 예상 한 정리 작업과 관련된 문제를 피할 수 있습니다.


질문에 대한 수정

복사 및 이동을 구현하는 방법은 @Raxvan이 지적한대로해야합니다.

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

std::moveT(other) 이미 rvalue이고 clang이 std::move 사용할 때 pessimisation에 대한 경고를 내 보냅니다.

개요

이동 할당 연산자가있는 경우 복사 및 스왑 및 복사 및 이동 간의 차이는 이동 할당보다 예외 안전성이 뛰어난 swap 메서드를 사용하고 있는지 여부에 따라 다릅니다. 표준 std::swap 의 경우 예외 안전은 복사 및 스왑 및 복사 및 이동간에 동일합니다. 대부분의 경우, swap 과 이동 지정에 동일한 예외 안전성 (항상은 아님)이있는 경우가 대부분이라고 생각합니다.

복사 및 이동을 구현하면 이동 할당 연산자가 없거나 잘못된 서명이있는 경우 복사 할당 연산자가 무한 재귀로 축소되는 위험이 있습니다. 그러나 적어도 clang은 이것에 대해 경고하고 컴파일러에게 -Werror=infinite-recursion 를 전달 -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;
    }
    

    에:

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




copy-and-swap