복사 및 교환 관용구는 무엇입니까? 좋아하는 C

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

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



답변

개요

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

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

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

어떻게 작동합니까?

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

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

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

(그뿐만 아니라 자격이없는 호출 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의 규칙은 우리의 복사 생성자, 할당 연산자 및 소멸자의 존재를 성공적으로 수반하지만 실제로는 “The Big Three and A Half”라고해야 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)

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

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


각주

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

†이 우리가 전문 것을 청구에있는 std::swap우리의 유형은 수준의 제공 swap에 따른 측 무료 기능 swap등,하지만이 모든 불필요한입니다 :의 적절한 사용이 swap지정되지 않은 호출을 통해 될 것이며, 우리의 기능이있을 것 ADL을 통해 발견 . 하나의 기능이 수행됩니다.

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

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


답변

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

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

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

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


답변

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

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

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

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;을 복사 및 구성 또는 할당 으로 구현해야 합니다. 던질 수 있지만 일부 데이터 멤버를 교체하지 않고 다른 멤버는 스왑하지 않은 상태로 남아있을 수 있습니다. 이 잠재력 std::string은 James가 다른 답변에 대해 언급 한 것처럼 C ++ 03에도 적용됩니다 .

@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지점) 매크로 ALA #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=있는지 확인하고 (필요한 경우 “재설정”코드를 보내는 경우도 있음), 복사 및 스왑 방식은 복사 생성자를 호출하여 열도록 작성 될 수 있습니다. 별도의 소켓 연결 후 원래 연결을 닫습니다. 이는 단순한 in-process 변수 복사본 대신 원격 네트워크 상호 작용을 의미 할뿐만 아니라 소켓 리소스 또는 연결에 대한 클라이언트 또는 서버 제한을 ​​무시할 수 있습니다. (물론이 클래스는 꽤 ​​무시 무시한 인터페이스를 가지고 있지만 그것은 또 다른 문제입니다. ;-P).*thisrhs


답변

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

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

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

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

}

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

여기에 이미지 설명을 입력하십시오

이것은 friend함수가 호출되고 this객체가 매개 변수로 전달되는 것과 관련이 있습니다.


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

void swap(A& other) {

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

}

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

여기에 이미지 설명을 입력하십시오


결국, 2 개의 객체를 교환 하기 위해 함수를 사용할 필요 는 없습니다 friend. swap하나의 멤버 함수 를 만드는 것만큼이나 의미 가 있습니다.other 객체를 매개 변수로 갖는 좋습니다.

이미 this객체에 액세스 할 수 있으므로 매개 변수로 전달하면 기술적으로 중복됩니다.


답변

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제공하는 것입니다 a상태 b초기했다입니다. 하면 어떻게됩니까 그러나, 숨겨진 문제가있다 a.get_allocator() != b.get_allocator()? 대답은 다음과 같습니다. 작성합시다 AT = std::allocator_traits<A>.

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

  • 경우 AT::propagate_on_container_swap이며 std::true_type, 다음 fs예상되는 방식으로 데이터와 할당자를 바꿉니다.

  • AT::propagate_on_container_swapis std::false_type인 경우 동적 검사가 필요합니다.

    • 인 경우 a.get_allocator() == b.get_allocator()두 컨테이너가 호환 가능한 스토리지를 사용하고 일반적인 방식으로 스와핑이 진행됩니다.
    • 그러나이면 a.get_allocator() != b.get_allocator()프로그램에 정의되지 않은 동작이 있습니다 ([container.requirements.general / 8] 참조).

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


답변