etc./StackOverFlow

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

청렴결백한 만능 재주꾼 2021. 11. 30. 23:59
반응형

질문자 :GManNickG


이 관용구는 무엇이며 언제 사용해야 합니까? 어떤 문제를 해결합니까? C++11을 사용하면 관용구가 바뀌나요?

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



개요

copy-and-swap 관용구가 필요한 이유는 무엇입니까?

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

copy-and-swap 관용구 가 솔루션이며 할당 연산자가 코드 중복을 피하고 강력한 예외 보장을 제공하는 두 가지를 달성하도록 우아하게 지원합니다.

어떻게 작동합니까?

개념적 으로 이것은 복사 생성자의 기능을 사용하여 데이터의 로컬 복사본을 만든 다음 스왑 기능으로 복사된 데이터를 가져와서 이전 데이터를 새 데이터 swap 그런 다음 임시 복사본은 오래된 데이터와 함께 파괴됩니다. 새 데이터의 복사본이 남습니다.

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

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

작동하는 이유는 무엇입니까?

먼저 중요한 선택이 있음을 알 수 있습니다. 매개변수 인수는 값으로 사용 됩니다. 다음과 같이 쉽게 할 수 있지만 (실제로 많은 순진한 관용구 구현이 수행합니다):

 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로 초기화 되면 move-constructed 가 됩니다. 완벽한. C++03에서 값으로 인수를 사용하여 복사 생성자 기능을 재사용할 수 있는 것과 같은 방식으로 C++11은 적절할 때 자동으로 이동 생성자를 선택합니다. (물론 앞서 링크된 글에서 언급했듯이 값의 복사/이동은 아예 생략할 수도 있습니다.)

이렇게 해서 복사 및 교환 관용구를 마칩니다.


각주

mArray 를 null로 설정하는 이유는 무엇입니까? 연산자의 추가 코드가 throw되면 dumb_array 의 소멸자가 호출될 수 있기 때문입니다. null로 설정하지 않고 그런 일이 발생하면 이미 삭제된 메모리를 삭제하려고 시도합니다! null을 삭제하는 것은 작업이 아니므로 null로 설정하여 이를 방지합니다.

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

‡이유는 간단합니다. 리소스가 있으면 필요한 곳이면 어디든지 교환 및/또는 이동할 수 있습니다(C++11). 그리고 매개변수 목록에 복사본을 만들어 최적화를 극대화합니다.

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


GManNickG

할당의 핵심은 두 단계입니다. 객체의 이전 상태를 해체 하고 새 상태를 다른 객체 상태의 복사본으로 구축하는 것입니다.

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

세련된 형태로 복사 및 교환은 할당 연산자의 (비참조) 매개변수를 초기화하여 복사를 수행함으로써 구현됩니다.

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

sbi

이미 좋은 답변이 있습니다. 나는 그들이 부족하다고 생각하는 것에 주로 초점을 맞출 것입니다 - copy-and-swap 관용구와 함께 "단점"에 대한 설명....

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

스왑 기능의 관점에서 할당 연산자를 구현하는 방법:

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

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

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

  • 새로운 값의 복사본이 만들어지면 객체의 현재 상태(즉, *this )를 수정하기 전에 획득을 시도할 수 있습니다. rhs 가 참조가 아닌 값 으로 수락되는(즉, 복사된) 이유입니다.

  • rhs 의 상태를 교환하고 *this 일반적 으로 잠재적인 실패/예외 없이 비교적 쉽게 수행할 수 있습니다. 로컬 복사본은 이후에 특정 상태가 필요하지 않습니다(소멸자가 실행되기 위한 상태 적합만 필요합니다. >= C++11에서 이동 중인 개체)

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

  • 강력한 예외 보장과 이상적으로는 실패/ throw swap 이 있거나 쓸 수 있다고 가정하고 예외를 throw하는 할당의 영향을 받지 않는 대상 객체를 원할 때 ..†

  • swap 및 소멸자 함수의 관점에서 할당 연산자를 정의하는 명확하고 이해하기 쉽고 강력한 방법을 원할 때.

    • 복사 및 교환 방식으로 자체 할당을 수행하여 종종 간과되는 경우를 방지할 수 있습니다.‡

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

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

@wilhelmtell: C++03에는 std::string::swap(std::swap에 의해 호출됨)에 의해 잠재적으로 throw되는 예외에 대한 언급이 없습니다. C++0x에서 std::string::swap은 noexcept이며 예외를 throw해서는 안 됩니다. – James McNellis 10년 12월 22일 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'에 자체 할당을 시도합니다!


⁂ 복사 및 교환 관용구는 추가 임시(연산자의 매개변수가 복사로 구성된 경우)의 사용으로 인해 비효율성 또는 제한을 도입할 수 있습니다.

 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 와 동일한 서버에 연결되어 있는지 확인할 수 있습니다(유용한 경우 "재설정" 코드를 보낼 수도 있음). 반면 copy-and-swap 접근 방식은 복사를 호출합니다. 고유한 소켓 연결을 연 다음 원래 연결을 닫도록 작성될 가능성이 있는 생성자. 이는 단순한 프로세스 내 변수 복사 대신 원격 네트워크 상호 작용을 의미할 뿐만 아니라 소켓 리소스 또는 연결에 대한 클라이언트 또는 서버 제한을 위반하여 실행할 수 있습니다. (물론 이 클래스는 매우 끔찍한 인터페이스를 가지고 있지만 그것은 또 다른 문제입니다 ;-P).


Tony Delroy

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

일부 버전의 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 함수를 사용할 필요 가 없습니다. 매개변수로 other 객체가 있는 멤버 함수를 swap 하는 것만큼이나 의미가 있습니다.

this 개체에 액세스할 수 있으므로 매개 변수로 전달하는 것은 기술적으로 중복됩니다.


Oleksiy

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_assignmentstd::true_type 이면 fm b.get_allocator() 값으로 a 의 할당자를 재할당하고, 그렇지 않으면 a 는 원래 할당자를 계속 사용합니다. 이 경우, 데이터 요소를 저장하기 때문에, 개별적으로 교체 될 필요가 및 a b 호환되지 않는다.

  • AT::propagate_on_container_swapstd::true_type 이면 fs 는 예상되는 방식으로 데이터와 할당자를 모두 교환합니다.

  • AT::propagate_on_container_swapstd::false_type 이면 동적 검사가 필요합니다.

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

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


Kerrek SB

출처 : http:www.stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom

반응형