복사 및 교환 관용구는 무엇입니까

c++ copy-constructor assignment-operator c++-faq copy-and-swap


이 관용구는 무엇이며 언제 사용해야합니까? 어떤 문제가 해결됩니까? C ++ 11을 사용할 때 관용구가 변경됩니까?

많은 곳에서 언급되었지만, 우리는 "무엇입니까"라는 질문과 답변이 없었으므로 여기에 있습니다. 다음은 이전에 언급 한 장소의 일부 목록입니다.




Answer 1 GManNickG


Overview

복사 및 교체 관용구가 필요한 이유는 무엇입니까?

자원을 관리하는 모든 클래스 ( 스마트 포인터와 같은 랩퍼 )는 The Big Three 를 구현해야합니다 . 복사 생성자와 소멸자의 목표와 구현은 간단하지만 복사 할당 연산자는 가장 미묘하고 어렵다. 어떻게해야합니까? 어떤 함정을 피해야합니까?

복사 및 스왑 관용구 의 해결책이 우아 두 가지를 달성 대입 연산자를 지원합니다 : 피 코드 중복을 , 그리고 제공하는 강력한 예외 보증을 .

어떻게 작동합니까?

개념적으로 복사 생성자의 기능을 사용하여 데이터의 로컬 사본을 작성한 다음 swap 기능으로 복사 된 데이터를 가져 와서 이전 데이터를 새 데이터로 교체합니다. 그런 다음 임시 사본은 오래된 데이터를 가져 와서 폐기합니다. 새 데이터의 사본이 남았습니다.

복사 및 스왑 이디엄을 사용하려면 작업중인 복사 생성자, 작업중인 소멸자 (둘 다 래퍼의 기초이므로 어쨌든 완료해야 함)와 swap 기능의 세 가지가 필요 합니다.

스왑 함수는 멤버의 멤버 인 클래스의 두 개체를 스왑 하는 비 투사 함수입니다. 우리는 우리 자신의 것을 제공하는 대신 std::swap 을 사용 하고 싶을 수도 있지만 이것은 불가능합니다. std::swap 은 구현 내에서 copy-constructor 및 copy-assignment 연산자를 사용하며 궁극적으로 자체적으로 관점에서 할당 연산자를 정의하려고합니다!

(그뿐만 아니라 swap 자격이없는 호출 은 std::swap 이 수반 하는 클래스의 불필요한 구성 및 파괴를 건너 뛰고 사용자 정의 스왑 연산자를 사용합니다 .


심층적 인 설명

목표

구체적인 사례를 생각해 봅시다. 쓸모없는 클래스에서 동적 배열을 관리하려고합니다. 작업 생성자, 복사 생성자 및 소멸자로 시작합니다.

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

이 클래스는 배열을 거의 성공적으로 관리하지만 올바르게 작동 하려면 operator= 가 필요 합니다.

실패한 솔루션

순진한 구현 방식은 다음과 같습니다.

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

그리고 우리는 우리가 끝났다고 말합니다. 이제 누출없이 어레이를 관리합니다. 그러나 코드에서 (n) 으로 순차적으로 표시되는 세 가지 문제가 있습니다 .

  1. 첫 번째는 자체 할당 테스트입니다. 이 검사는 두 가지 목적으로 사용됩니다. 자체 할당에서 불필요한 코드를 실행하지 못하게하는 쉬운 방법이며, 미묘한 버그 (예 : 배열을 삭제하여 복사하여 복사하는 것)로부터 보호합니다. 그러나 다른 모든 경우에는 단순히 프로그램 속도를 늦추고 코드에서 노이즈 역할을합니다. 자체 할당은 거의 발생하지 않으므로이 검사는 대부분 낭비입니다. 작업자가 없이도 제대로 작동 할 수 있다면 더 좋습니다.

  2. 두 번째는 기본적인 예외 보장 만 제공한다는 것입니다. 경우 new int[mSize] 실패 *this 수정 된 것입니다. (즉, 크기가 잘못되었고 데이터가 사라졌습니다!) 강력한 예외 보장을 위해서는 다음과 비슷한 것이 필요합니다.

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
  3. 코드가 확장되었습니다! 이는 코드 중복이라는 세 번째 문제로 이어집니다. 우리의 할당 연산자는 우리가 이미 다른 곳에서 작성한 모든 코드를 효과적으로 복제합니다. 끔찍한 일입니다.

우리의 경우 그 핵심은 두 줄 (할당 및 사본)이지만 더 복잡한 리소스를 사용하면이 코드 팽창이 번거로울 수 있습니다. 우리는 결코 반복하지 않기 위해 노력해야합니다.

(하나의 리소스를 올바르게 관리하기 위해 많은 양의 코드가 필요한 경우 클래스가 둘 이상을 관리하는 경우 어떻게 될까요? 이것은 유효한 우려로 보일 수 있지만 실제로는 사소한 try / catch 절이 필요합니다. 문제는 아닙니다. 클래스가 하나의 리소스 만 관리해야하기 때문입니다 !)

성공적인 솔루션

언급했듯이 복사 및 스왑 숙어는 이러한 모든 문제를 해결합니다. 그러나 현재 swap 기능을 제외한 모든 요구 사항이 있습니다. 세 가지의 규칙이 성공적으로 복사 생성자, 대입 연산자, 소멸자의 존재를 수반하지만, 정말 '빅 3 년 반 "라고해야합니다 언제든지 당신의 클래스는 또한 의미가 제공 할 수 자원 관리 swap 함수.

클래스에 스왑 기능을 추가해야하며 다음과 같이합니다. † :

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( 여기public friend swap 이 필요한 이유가 설명되어 있습니다.) 이제 dumb_array 를 교환 할 수있을뿐만 아니라 일반적으로 교환이 더 효율적일 수 있습니다. 전체 배열을 할당하고 복사하는 대신 포인터와 크기 만 교환합니다. 기능 및 효율성의이 보너스 외에도 이제 복사 및 스왑 숙어를 구현할 준비가되었습니다.

더 이상 고민하지 않고 할당 연산자는 다음과 같습니다.

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

그리고 그게 다야! 한 번의 실패로 세 가지 문제는 모두 한 번에 우아하게 해결됩니다.

왜 작동합니까?

우리는 먼저 중요한 선택을 알아 냈습니다 : 매개 변수 인수는 by-value 입니다. 다음과 같이 쉽게 할 수 있지만 실제로는 많은 순진한 관용구 구현이 가능합니다.

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

우리는 중요한 최적화 기회를 잃 습니다 . 뿐만 아니라이 선택은 C ++ 11에서 매우 중요합니다. (일반적으로 주목할만한 유용한 지침은 다음과 같습니다. 함수에서 무언가의 복사본을 만들려면 컴파일러가 매개 변수 목록에서 수행하게하십시오. ‡)

어느 쪽이든, 자원을 얻는이 방법은 코드 복제를 제거하는 열쇠입니다. 우리는 복사 생성자에서 코드를 사용하여 복사를 수행하며, 비트를 반복 할 필요가 없습니다. 이제 복사가 완료되었으므로 교체 할 준비가되었습니다.

기능을 입력 할 때 모든 새 데이터가 이미 할당, 복사 및 사용할 준비가되어 있는지 확인하십시오. 이것은 우리에게 무료로 강력한 예외 보장을 제공하는 것입니다. 복사 구성에 실패하면 함수에 들어 가지 않을 것이므로 *this 상태를 변경할 수 없습니다 . (강력한 예외 보장을 위해 이전에 수동으로 한 일, 컴파일러는 지금 우리를 위해하고 있습니다. 얼마나 친절합니다.)

swap 이 발생하지 않기 때문에이 시점에서 우리는 집이 없습니다 . 현재 데이터를 복사 된 데이터와 교체하여 상태를 안전하게 변경하면 기존 데이터가 임시로 저장됩니다. 그런 다음 함수가 반환되면 이전 데이터가 해제됩니다. (매개 변수의 범위가 끝나고 소멸자가 호출되는 위치)

관용구는 코드를 반복하지 않기 때문에 연산자 내에 버그를 도입 할 수 없습니다. 이것은 우리가 operator= 의 단일 균일 한 구현을 허용하는 자체 할당 점검의 필요성을 제거한다는 것을 의미한다 . 또한 자체 할당이 아닌 경우 더 이상 성능이 저하되지 않습니다.

이것이 바로 복사 및 교환 관용구입니다.

C ++ 11은 어떻습니까?

다음 버전의 C ++ 인 C ++ 11은 리소스 관리 방식을 매우 중요하게 변경합니다. 3 의 규칙 은 이제 4의 규칙 (반)입니다. 왜? 리소스를 복사 구성 할 수 있어야 할 뿐만 아니라 이동 구성도해야합니다 .

운 좋게도 이것은 쉽다 :

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

무슨 일이야? 이동 구성의 목표를 기억하십시오. 클래스의 다른 인스턴스에서 자원을 가져 와서 할당 가능하고 파괴 가능한 상태로 유지하십시오.

우리가 한 일은 간단합니다. 기본 생성자 (C ++ 11 기능)를 통해 초기화 한 다음 other 와 교체하십시오 . 우리는 클래스의 기본 생성 인스턴스가 안전하게 할당되고 파괴 될 수 있다는 것을 알고 있으므로 스왑 후 other 클래스도 동일하게 수행 할 수 있음을 알고 있습니다.

(일부 컴파일러는 생성자 위임을 지원하지 않습니다.이 경우 클래스를 수동으로 기본 구성해야합니다. 불행히도 운이 좋지 않은 작업입니다.)

왜 작동합니까?

그것이 우리 수업에 필요한 유일한 변화이므로 왜 효과가 있습니까? 매개 변수를 참조가 아닌 값으로 만들기 위해 결정한 가장 중요한 결정을 기억하십시오.

dumb_array& operator=(dumb_array other); // (1)

이제 other 값을 rvalue로 초기화 하면 이동 구성 됩니다. 완전한. C ++ 03과 같은 방식으로 인수 값을 사용하여 복사 생성자 기능을 재사용 할 수 있습니다. C ++ 11은 적절한 경우 이동 생성자를 자동으로 선택합니다. (물론, 이전에 링크 된 기사에서 언급했듯이, 가치의 복사 / 이동은 단순히 제거 될 수 있습니다.)

그리고 복사 및 스왑 숙어를 마칩니다.


Footnotes

* mArray 를 null로 설정하는 이유는 무엇 입니까? 연산자의 추가 코드가 발생하면 dumb_array 의 소멸자가 호출 될 수 있습니다. 그리고 null로 설정하지 않고 발생하면 이미 삭제 된 메모리를 삭제하려고 시도합니다! null을 삭제하면 작동하지 않으므로 null로 설정하여이를 방지합니다.

†이 우리가 전문 것을 청구에 있습니다 std::swap 우리의 유형은 동급 제공, swap 을 따라 측 무료 기능 swap 등,하지만이 모든 불필요한입니다 :의 적절한 사용 swap 적정을 통해 될 것입니다 호출하면 ADL을 통해 함수를 찾을 수 있습니다 . 하나의 기능이 수행됩니다.

‡ 이유는 간단합니다. 일단 리소스를 확보 한 후에는 필요할 때마다 리소스를 교환 및 / 또는 이동할 수 있습니다 (C ++ 11). 그리고 매개 변수 목록에 사본을 작성하면 최적화를 최대화 할 수 있습니다.

†† 이동 생성자는 일반적으로 noexcept 이어야합니다 . 그렇지 않으면 이동이 의미가있는 경우에도 일부 코드 (예 : std::vector resizing logic)가 복사 생성자를 사용합니다. 물론 내부 코드에서 예외가 발생하지 않는 경우에만 예외로 표시하십시오.




Answer 2 sbi


: 할당, 그 중심에, 두 단계 인 객체의 이전 상태를 찢어 하고 사본으로 새로운 상태를 만드는 다른 객체의 상태.

기본적으로 이것이 소멸자복사 생성자 가하는 일이므로 첫 번째 아이디어는 작업을 위임하는 것입니다. 그러나, 파괴가 반드시 실패해서는 안되므로, 건설은 가능하지만, 실제로는 다른 방법으로하고 싶습니다 : 먼저 건설적인 부분을 수행 하고, 성공한 경우 에는 파괴적인 부분을 수행하십시오 . copy-and-swap 이디엄은이를 수행하는 방법입니다. 먼저 클래스의 복사 생성자를 호출하여 임시 객체를 만든 다음 해당 데이터를 임시 객체와 교체 한 다음 임시 소멸자가 이전 상태를 파괴하도록합니다.
swap() 은 절대 실패하지 않기 때문에 실패 할 수있는 유일한 부분은 복사 구성입니다. 먼저 수행되고 실패하면 대상 객체에서 아무것도 변경되지 않습니다.

정제 된 형식으로 복사 및 스왑은 할당 연산자의 (비 참조) 매개 변수를 초기화하여 복사를 수행하여 구현됩니다.

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}



Answer 3 Tony Delroy


이미 좋은 답변이 있습니다. 나는 그들이 부족하다고 생각하는 것에 주로 초점을 맞출 것입니다-복사 및 스왑 숙어를 가진 "단점"에 대한 설명 ....

복사 및 교환 관용구는 무엇입니까?

스왑 기능으로 할당 연산자를 구현하는 방법 :

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

기본 아이디어는 다음과 같습니다.

  • 객체에 할당 할 때 오류가 발생하기 쉬운 부분은 새로운 상태에 필요한 모든 리소스 (예 : 메모리, 설명자)를 확보하는 것입니다.

  • 새로운 값의 사본이 만들어 지면 객체의 현재 상태 (즉, *this ) 수정 하기 전에 획득을 시도 할 수 있습니다. 따라서 rhs참조가 아닌 (예 : 복사)으로 허용 됩니다.

  • 로컬 복사의 상태 스와핑 rhs 하고 *this 이다 일반적으로 비교적 쉽게 나중에 특정 상태를 필요로하지 않는다 로컬 복사본 주어 잠재적 인 장애 / 예외없이 할 수있는 (단지 많은에 관해서는, 실행에 소멸자에 대한 상태에 맞는 필요 > = C ++ 11에서 이동 되는 객체 )

언제 사용해야합니까? (어떤 문제가 [/ create]를 해결 합니까?)

  • 당신이 원하는 때 당신이 나 쓸 수있는 가정, 예외가 발생 할당에 의해 영향을받지 반대에 할당 된 swap 강력한 예외 보증과 함께, 그리고 / 실패 할 수없는 이상적으로 하나 throw ... †

  • (더 간단한) 복사 생성자, swap 및 소멸자 함수로 할당 연산자를 정의하는 깨끗하고 이해하기 쉽고 강력한 방법을 원할 때 .

    • 복사 및 스왑으로 수행되는 자체 할당 기능은 간과되는 간과를 피합니다. ‡

  • 할당 중에 추가 임시 개체를 생성하여 생성 된 성능 저하 또는 일시적으로 더 높은 리소스 사용이 응용 프로그램에 중요하지 않은 경우 ⁂

swap 던지기 : 일반적으로 개체가 포인터로 추적하는 데이터 멤버를 안정적으로 스왑 할 수 있지만 스로 핑이없는 스왑이 없거나 스왑을 X tmp = lhs; lhs = rhs; rhs = tmp; 로 구현해야하는 비 포인터 데이터 멤버입니다 . lhs = rhs; rhs = tmp; 복사 구성 또는 할당이 발생할 수 있지만 여전히 일부 데이터 멤버를 교환하지 않고 다른 멤버는 교환하지 못할 가능성이 있습니다. 이 잠재력 은 James가 다른 답변에 대해 언급 한 것처럼 C ++ 03 std::string 에도 적용됩니다 .

@wilhelmtell : C ++ 03에는 std :: string :: swap (std :: swap에 의해 호출 됨)에서 발생할 수있는 예외에 대한 언급이 없습니다. C ++ 0x에서 std :: string :: swap은 예외가 아니며 예외를 발생시키지 않아야합니다. – James McNellis 12 월 22 일 10시 15 분 24 초


고유 한 개체에서 할당 할 때 제정신이 아닌 할당 연산자 구현은 자체 할당을 위해 쉽게 실패 할 수 있습니다. 클라이언트 코드가 자체 할당을 시도하는 것은 상상조차 할 수 없지만 x = f(x); 부호 f (아마도 단지 일부이다 #ifdef 와의 분기) 매크로 람 #define f(x) x 또는 참조 반환하는 함수 x 추천, 또는 (아마도 비효율적하지만 간결한) 부호 x = c1 ? x * 2 : c2 ? x / 2 : x; ). 예를 들면 다음과 같습니다.

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

자체 할당에서 위 코드는 x.p_; 삭제합니다 . , p_ 가 새로 할당 된 힙 영역을 가리키고 그 안에 초기화되지 않은 데이터 (정의되지 않은 동작) 를 읽으려고 시도합니다 . 너무 이상하지 않은 경우 copy 는 방금 파괴 된 모든 'T'에 자체 할당을 시도합니다!


copy 복사 및 교체 관용구는 여분의 임시 사용으로 인해 비 효율성이나 제한 사항이 발생할 수 있습니다 (운영자 매개 변수가 복사 구성되어있는 경우).

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

여기서, 손으로 쓴 Client::operator=*this 이미 rhs 와 동일한 서버에 연결되어 있는지 (유용한 경우 아마도 "재설정"코드를 보내는 것임) 확인할 수 있지만, 복사 및 스왑 방식은 복사를 호출합니다. 별개의 소켓 연결을 열고 원래 연결을 닫도록 작성되었을 가능성이있는 생성자. 이는 단순한 in-process 변수 복사본 대신 원격 네트워크 상호 작용을 의미 할뿐만 아니라 소켓 리소스 또는 연결에 대한 클라이언트 또는 서버 제한을 ​​무시할 수 있습니다. (물론이 클래스는 꽤 ​​무시 무시한 인터페이스를 가지고 있지만 그것은 또 다른 문제입니다. ;-P).




Answer 4 Oleksiy


이 답변은 위의 답변에 대한 추가 및 약간의 수정과 비슷합니다.

Visual Studio의 일부 버전 (및 다른 컴파일러)에는 실제로 성 가시고 의미가없는 버그가 있습니다. 따라서 다음 과 같이 swap 함수 를 선언 / 정의하면

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... swap 함수 를 호출하면 컴파일러에서 소리를 지 릅니다 .

enter image description here

이것은 함께 할 수있는 뭔가가 friend 함수가 호출되는 및 this 객체가 매개 변수로 전달되고있다.


이를 해결하는 방법은 friend 키워드를 사용하지 않고 swap 기능을 재정의하는 것입니다.

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

이번에는 swap 을 호출 하고 other 를 전달 하면 컴파일러가 행복해집니다.

enter image description here


결국, friend 기능을 사용하여 2 개의 객체를 바꿀 필요 는 없습니다 . swap 을 하나의 other 객체를 매개 변수로 갖는 멤버 함수 로 만드는 것이 좋습니다.

이미에 액세스 할 수있는 this 매개 변수로에 전달하는 것은 기술적으로 중복하므로, 개체를.




Answer 5 Kerrek SB


C ++ 11 스타일의 할당 자 인식 컨테이너를 다룰 때 경고 단어를 추가하고 싶습니다. 스와핑과 할당은 미묘하게 다른 의미를 가지고 있습니다.

구체적으로 컨테이너 std::vector<T, A> 고려해 봅시다 . 여기서 A 는 상태 저장 할당 자 유형이며 다음 함수를 비교합니다.

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

함수 fsfm 의 목적은 b 에 초기 상태 를 부여 a 것 입니다. 그러나 숨겨진 질문이 있습니다. a.get_allocator() != b.get_allocator() 어떻게됩니까? 대답은 다음과 같습니다. AT = std::allocator_traits<A> 작성해 봅시다 .

  • 만약 AT::propagate_on_container_move_assignment IS std::true_type 다음 fm 의 다시 할당, 할당 의 값 b.get_allocator() , 그렇지 않으면하지 않으며, a 원래의 할당을 계속 사용한다. 이 경우 ab 의 스토리지 가 호환되지 않기 때문에 데이터 요소를 개별적으로 교체해야합니다 . a

  • 만약 AT::propagate_on_container_swap IS std::true_type , 다음 fs 스왑 예상되는 방식으로 데이터와 할당자를.

  • 경우 AT::propagate_on_container_swap 있다 std::false_type , 우리는 동적 인 검사가 필요합니다.

    • 경우 a.get_allocator() == b.get_allocator() , 다음이 개 컨테이너는 호환 스토리지 및 일반적인 방식으로 교환 진행을 사용합니다.
    • 그러나 a.get_allocator() != b.get_allocator() 인 경우 프로그램에 정의되지 않은 동작이 있습니다 ([container.requirements.general / 8] 참조).

결론은 컨테이너가 상태 저장 할당자를 지원하기 시작하자마자 스왑이 C ++ 11에서 사소한 작업이되었다는 것입니다. 그것은 다소 "고급 사용 사례"이지만, 클래스가 리소스를 관리하고 메모리가 가장 인기있는 리소스 중 하나라면 이동 최적화가 일반적으로 흥미로워지기 때문에 전적으로 가능성이 낮지는 않습니다.