함수 - 복사 및 스왑 관용구가 C++ 11의 복사 및 이동 관용구가되어야합니까?




복사생성자 사용이유 (2)

이 대답 에서 설명한 것처럼 복사 및 스왑 이디엄은 다음과 같이 구현됩니다.

class MyClass
{
private:
    BigClass data;
    UnmovableClass *dataPtr;

public:
    MyClass()
      : data(), dataPtr(new UnmovableClass) { }
    MyClass(const MyClass& other)
      : data(other.data), dataPtr(new UnmovableClass(*other.dataPtr)) { }
    MyClass(MyClass&& other)
      : data(std::move(other.data)), dataPtr(other.dataPtr)
    { other.dataPtr= nullptr; }

    ~MyClass() { delete dataPtr; }

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, other.data);
        swap(first.dataPtr, other.dataPtr);
    }

    MyClass& operator=(MyClass other)
    {
        swap(*this, other);
        return *this;
    }
};

operator =에 대한 매개 변수로 MyClass 값을 가짐으로써 매개 변수는 복사 생성자 또는 이동 생성자에 의해 생성 될 수 있습니다. 그런 다음 매개 변수에서 안전하게 데이터를 추출 할 수 있습니다. 이렇게하면 코드 중복을 방지하고 예외 안전을 지원합니다.

대답은 일시적으로 변수를 교환하거나 이동할 수 있음을 나타냅니다. 주로 스와핑에 대해 설명합니다. 그러나 컴파일러에 의해 최적화되지 않은 경우 스왑에는 세 가지 이동 작업이 포함되며 더 복잡한 경우 추가 추가 작업이 수행됩니다. 원하는 모든 것이 임시 개체를 할당 된 개체로 이동 하는 것입니다.

관찰자 패턴을 포함하는이보다 복잡한 예제를 고려하십시오. 이 예에서는 할당 연산자 코드를 직접 작성했습니다. 이동 생성자, 대입 연산자 및 스왑 메서드에 중점을 둡니다.

class MyClass : Observable::IObserver
{
private:
    std::shared_ptr<Observable> observable;

public:
    MyClass(std::shared_ptr<Observable> observable) : observable(observable){ observable->registerObserver(*this); }
    MyClass(const MyClass& other) : observable(other.observable) { observable.registerObserver(*this); }
    ~MyClass() { if(observable != nullptr) { observable->unregisterObserver(*this); }}

    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

    friend void swap(MyClass& first, MyClass& second)
    {
        //Checks for nullptr and same observable omitted
            using std::swap;
            swap(first.observable, second.observable);

            second.observable->unregisterObserver(first);
            first.observable->registerObserver(first);
            first.observable->unregisterObserver(second);
            second.observable->registerObserver(second);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }
}

분명히이 수동으로 작성된 대입 연산자의 코드 부분은 이동 생성자의 코드 부분과 동일합니다. 할당 연산자에서 스왑을 수행 할 수는 있지만 동작이 올바르지 만 잠재적으로 더 많은 이동을 수행하고 추가 등록 (스왑에서) 및 등록 해제 (소멸자에서)를 수행합니다.

대신에 생성자의 코드를 재사용하는 것이 더 합리적이지 않습니까?

private:
    void performMoveActions(MyClass&& other)
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

public:
    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        performMoveActions(other);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        performMoveActions(other);
    }

이 접근 방식이 스왑 방식보다 열등하지 않은 것처럼 보입니다. 복사 - 스왑 (copy-and-swap) 관용구가 C ++ 11에서 복사 및 이동 관용구로서 더 나을 것이라고 생각하는 것이 맞습니까? 아니면 중요한 것을 놓쳤습니까?


각 특별 회원에게 가치있는 부드러운 사랑의 보살핌을주고 가능한 한 많이 기본 설정하십시오.

class MyClass
{
private:
    BigClass data;
    std::unique_ptr<UnmovableClass> dataPtr;

public:
    MyClass() = default;
    ~MyClass() = default;
    MyClass(const MyClass& other)
        : data(other.data)
        , dataPtr(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                : nullptr)
        { }
    MyClass& operator=(const MyClass& other)
    {
        if (this != &other)
        {
            data = other.data;
            dataPtr.reset(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                        : nullptr);
        }
        return *this;
    }
    MyClass(MyClass&&) = default;
    MyClass& operator=(MyClass&&) = default;

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, second.data);
        swap(first.dataPtr, second.dataPtr);
    }
};

소멸자는 원하는 경우 위에 암시 적으로 기본값으로 설정할 수 있습니다. 이 예제에서는 다른 모든 것이 명시 적으로 정의되거나 기본값으로 설정되어야합니다.

참조 : http://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf

복사 / 스왑 관용구가 성능에 영향을 줄 수 있습니다 (슬라이드 참조). 예를 들어, 왜 고성능 / std::vectorstd::string copy / swap을 사용하지 않는지 자주 사용하는지 궁금합니다. 성능 저하로 그 이유가 있습니다. BigClassstd::vector 또는 std::string (가능성이있는 것)이 포함되어있는 경우 가장 좋은 방법은 특수 멤버에게 스페셜 멤버에게 전화하는 것입니다. 위의 방법을 수행하는 것입니다.

과제에서 강력한 예외 안전성이 필요한 경우 성능에 추가하여 제공하는 방법에 대한 슬라이드 ( "strong_assign"검색)를 참조하십시오.


우선, 클래스가 움직일 수있는 한 C ++ 11에서 swap 함수를 작성하는 것은 일반적으로 불필요합니다. 기본 swap 은 이동에 의존합니다.

void swap(T& left, T& right) {
    T tmp(std::move(left));
    left = std::move(right);
    right = std::move(tmp);
}

그리고 그게 요소들입니다.

둘째로, 이것에 기반하여, Copy-And-Swap은 실제로 여전히 있습니다 :

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

// Let's not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;

복사 및 스왑 (이동) 또는 이동 및 스왑 (이동)이며 항상 최적의 성능에 근접해야합니다. 몇 가지 여분의 과제가있을 수 있지만 잘하면 귀하의 컴파일러가 처리됩니다.

편집 : 이것은 복사 할당 연산자 만 구현합니다. 그렇지 않으면 스택 오버플로가 발생합니다 (move-assignment 및 swap이 서로를 무기한 호출 함).





copy-and-swap