이 관용구는 무엇이며 언제 사용해야 합니까? 어떤 문제를 해결합니까? C++11을 사용하면 관용구가 바뀌나요?
여러 곳에서 언급되었지만 "무엇이 무엇입니까"라는 질문과 대답이 없었으므로 여기에 있습니다. 이전에 언급된 장소의 일부 목록은 다음과 같습니다.
질문자 :GManNickG
이 관용구는 무엇이며 언제 사용해야 합니까? 어떤 문제를 해결합니까? C++11을 사용하면 관용구가 바뀌나요?
여러 곳에서 언급되었지만 "무엇이 무엇입니까"라는 질문과 대답이 없었으므로 여기에 있습니다. 이전에 언급된 장소의 일부 목록은 다음과 같습니다.
리소스를 관리하는 모든 클래스(스마트 포인터와 같은 래퍼 )는 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)
으로 순차적으로 표시된 세 가지 문제가 있습니다.
첫 번째는 자기주도적 시험입니다.
이 검사는 두 가지 목적을 수행합니다. 자체 할당에서 불필요한 코드를 실행하는 것을 방지하는 쉬운 방법이고 미묘한 버그(예: 어레이를 시도하고 복사하기 위해서만 어레이를 삭제하는 것)로부터 보호합니다. 그러나 다른 모든 경우에는 프로그램 속도를 늦추고 코드에서 노이즈로 작용할 뿐입니다. 자체 할당은 거의 발생하지 않으므로 대부분의 경우 이 검사는 낭비입니다.
운영자가 없이도 제대로 작동할 수 있다면 더 좋을 것입니다.
두 번째는 기본적인 예외 보장만 제공한다는 것입니다. 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; }
코드가 확장되었습니다! 이는 세 번째 문제인 코드 중복으로 이어집니다.
할당 연산자는 우리가 이미 다른 곳에서 작성한 모든 코드를 효과적으로 복제합니다. 이것은 끔찍한 일입니다.
우리의 경우 핵심은 단 두 줄(할당 및 복사)이지만 더 복잡한 리소스를 사용하면 이 코드 팽창이 상당히 번거로울 수 있습니다. 우리는 결코 반복하지 않도록 노력해야 합니다.
(하나의 리소스를 올바르게 관리하기 위해 이 정도의 코드가 필요한 경우, 내 클래스가 둘 이상의 리소스를 관리한다면 어떻게 될까요?
이것이 유효한 문제로 보일 수 있고 실제로 사소한 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++의 다음 버전인 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로 표시합니다.
할당의 핵심은 두 단계입니다. 객체의 이전 상태를 해체 하고 새 상태를 다른 객체 상태의 복사본으로 구축하는 것입니다.
기본적으로 그것이 소멸자 와 복사 생성자 가 하는 일이므로 첫 번째 아이디어는 작업을 그들에게 위임하는 것입니다. 그러나 파괴는 실패해서는 안 되지만 건설은 실패하지 않아야 하기 때문에 우리는 실제로 그 반대의 방법을 원합니다 . 먼저 건설 적인 부분을 수행하고 성공 하면 파괴적인 부분을 수행합니다 . 복사 및 교환 관용구는 바로 이를 수행하는 방법입니다. 먼저 클래스의 복사 생성자를 호출하여 임시 객체를 생성한 다음 해당 데이터를 임시 객체와 교환한 다음 임시 소멸자가 이전 상태를 파괴하도록 합니다.
swap()
은 절대 실패하지 않아야 하므로 실패할 수 있는 유일한 부분은 복사 구성입니다. 그것이 먼저 수행되고 실패하면 대상 개체에서 아무 것도 변경되지 않습니다.
세련된 형태로 복사 및 교환은 할당 연산자의 (비참조) 매개변수를 초기화하여 복사를 수행함으로써 구현됩니다.
T& operator=(T tmp) { this->swap(tmp); return *this; }
이미 좋은 답변이 있습니다. 나는 그들이 부족하다고 생각하는 것에 주로 초점을 맞출 것입니다 - 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).
이 답변은 위의 답변에 대한 추가 및 약간의 수정과 비슷합니다.
일부 버전의 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
개체에 액세스할 수 있으므로 매개 변수로 전달하는 것은 기술적으로 중복됩니다.
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); }
fs
및 fm
의 목적은 b
가 처음에 가졌던 상태를 제공 a
그러나 숨겨진 질문이 있습니다. a.get_allocator() != b.get_allocator()
이면 어떻게 됩니까? 대답은 다음과 같습니다. AT = std::allocator_traits<A>
작성해 봅시다.
AT::propagate_on_container_move_assignment
가 std::true_type
이면 fm
b.get_allocator()
값으로 a
의 할당자를 재할당하고, 그렇지 않으면 a
는 원래 할당자를 계속 사용합니다. 이 경우, 데이터 요소를 저장하기 때문에, 개별적으로 교체 될 필요가 및 a
b
호환되지 않는다.
AT::propagate_on_container_swap
이 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에서 중요한 작업이 되었다는 것입니다. 이것은 다소 "고급 사용 사례"이지만, 이동 최적화는 일반적으로 클래스가 리소스를 관리하고 메모리가 가장 인기 있는 리소스 중 하나일 때만 흥미로워지기 때문에 완전히 가능성이 없는 것은 아닙니다.
출처 : http:www.stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
Java의 문자열 값에서 열거형 값을 얻는 방법 (0) | 2021.11.30 |
---|---|
Git을 사용하여 이전 커밋에서 분기 (0) | 2021.11.30 |
Git 관리 프로젝트의 모든 로컬 변경 사항을 이전 상태로 되돌리려면 어떻게 해야 합니까? (0) | 2021.11.30 |
HTML의 id 속성에 유효한 값은 무엇입니까? (0) | 2021.11.30 |
Linux에서 echo의 출력 색상을 변경하는 방법 (0) | 2021.11.30 |