C++
3, 5, 0의 규칙
수색…
파이브의 규칙
C ++ 11에는 move constructor과 move assignment operator라는 두 개의 새로운 멤버 함수가 새로 추가되었습니다. C ++ 03에서 Rule of Three 를 따르기를 원하는 동일한 이유로, 일반적으로 C ++ 11의 Rule of Five를 따르기를 원합니다. 클래스가 다섯 개의 특수 멤버 함수 중 하나를 요구하고 move semantics 원하는 경우, 그 중 5 개가 모두 필요할 것입니다.
그러나 5의 규칙을 지키지 않는 것은 일반적으로 오류로 간주되지 않지만 3 가지 규칙이 계속 준수되는 한 놓친 최적화 기회로 간주됩니다. 컴파일러에서 일반적으로 사용하는 이동 생성자 또는 이동 대입 연산자가 없으면 가능한 경우 복사 의미를 사용하므로 불필요한 복사 작업으로 인해 작업의 효율성이 떨어집니다. 클래스에 이동 시맨틱이 필요하지 않으면 이동 생성자 또는 할당 연산자를 선언 할 필요가 없습니다.
규칙 3의 경우와 같은 예 :
class Person
{
char* name;
int age;
public:
// Destructor
~Person() { delete [] name; }
// Implement Copy Semantics
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment.
Person copy(other);
swap(*this, copy);
return *this;
}
// Implement Move Semantics
// Note: It is usually best to mark move operators as noexcept
// This allows certain optimizations in the standard library
// when the class is used in a container.
Person(Person&& that) noexcept
: name(nullptr) // Set the state so we know it is undefined
, age(0)
{
swap(*this, that);
}
Person& operator=(Person&& that) noexcept
{
swap(*this, that);
return *this;
}
friend void swap(Person& lhs, Person& rhs) noexcept
{
std::swap(lhs.name, rhs.name);
std::swap(lhs.age, rhs.age);
}
};
또는 복사 및 이동 할당 연산자는 복사 및 스왑 이디엄 사용을 용이하게하기 위해 참조 또는 rvalue 참조 대신 값으로 인스턴스를 취하는 단일 할당 연산자로 대체 될 수 있습니다.
Person& operator=(Person copy)
{
swap(*this, copy);
return *this;
}
3의 규칙에서 5의 규칙으로 확장하는 것은 성능상의 이유로 중요하지만 대부분의 경우 엄격하게 필요하지는 않습니다. 복사 생성자와 대입 연산자를 추가하면 형식을 이동해도 메모리가 누출되지 않습니다 (이동 구성은이 경우 단순히 복사로 되돌아갑니다)하지만 호출자가 예상하지 못한 복사본을 수행하게됩니다.
제로 규칙
우리는 Rule of Five와 RAII 의 원칙을 결합하여 더 가벼운 인터페이스를 얻을 수 있습니다. Rule of Zero : 관리해야하는 리소스는 자체 유형이어야합니다. 이 유형은 Rule of Five를 따라야하지만, 해당 자원의 모든 사용자는 다섯 개의 특수 멤버 함수 중 하나를 쓸 필요가 없으며 간단히 모든 멤버를 default
설정할 수 있습니다.
Rule of Three 예제 에서 소개 된 Person
클래스를 사용하여 cstrings
대한 자원 관리 객체를 만들 수 있습니다.
class cstring {
private:
char* p;
public:
~cstring() { delete [] p; }
cstring(cstring const& );
cstring(cstring&& );
cstring& operator=(cstring const& );
cstring& operator=(cstring&& );
/* other members as appropriate */
};
일단 이것이 분리되면 Person
클래스가 훨씬 더 단순 해집니다.
class Person {
cstring name;
int arg;
public:
~Person() = default;
Person(Person const& ) = default;
Person(Person&& ) = default;
Person& operator=(Person const& ) = default;
Person& operator=(Person&& ) = default;
/* other members as appropriate */
};
Person
의 특별 멤버는 명시 적으로 선언 할 필요가 없습니다. 컴파일러는 Person
의 내용에 따라 적절하게 기본값을 설정하거나 삭제합니다. 따라서 다음은 0 규칙의 예입니다.
struct Person {
cstring name;
int arg;
};
cstring
이 delete
d 복사 생성자 / 대입 연산자가있는 이동 전용 유형 Person
경우 Person
도 자동으로 move-only가됩니다.
기간 제로의 용어는 R. Martinho Fernandes
3 규칙
Rule of Three는 타입이 사용자 정의 복사 생성자, 복사 할당 연산자 또는 소멸자를 가질 필요가있는 경우 세 가지 모두 를 가져야한다고 규정합니다.
규칙의 이유는 3 가지를 필요로하는 클래스가 리소스 (파일 핸들, 동적으로 할당 된 메모리 등)를 관리하고,이 리소스를 일관되게 관리하기 위해 세 가지 모두가 필요하기 때문입니다. 복사 함수는 리소스가 객체간에 복사되는 방법을 처리하며 소멸자는 RAII 원칙 에 따라 리소스를 파괴합니다.
문자열 리소스를 관리하는 유형을 생각해보십시오.
class Person
{
char* name;
int age;
public:
Person(char const* new_name, int new_age)
: name(new char[std::strlen(new_name) + 1])
, age(new_age)
{
std::strcpy(name, new_name);
}
~Person() {
delete [] name;
}
};
생성자에 name
이 할당 되었기 때문에 소멸자는 메모리 누수를 피하기 위해 할당을 해제합니다. 그러나 그러한 객체가 복사되면 어떻게됩니까?
int main()
{
Person p1("foo", 11);
Person p2 = p1;
}
먼저, p1
이 구축 될 것입니다. 그러면 p2
가 p1
에서 복사됩니다. 그러나 C ++ 생성 복사 생성자는 해당 형식의 각 구성 요소를 그대로 복사합니다. 즉, p1.name
과 p2.name
모두 동일한 문자열을 가리 킵니다.
main
끝나면 소멸자가 호출됩니다. 첫 번째 p2
의 소멸자가 호출됩니다. 문자열을 삭제합니다. 그러면 p1
의 소멸자가 호출됩니다. 그러나 문자열은 이미 삭제되었습니다 . 이미 삭제 된 메모리에서 delete
를 호출하면 정의되지 않은 동작이 발생합니다.
이를 피하려면 적합한 복사 생성자를 제공해야합니다. 하나의 접근법은 서로 다른 Person
인스턴스가 동일한 문자열 데이터를 공유하는 참조 계산 시스템을 구현하는 것입니다. 복사본이 수행 될 때마다 공유 참조 횟수가 증가합니다. 그런 다음 소멸자는 참조 횟수를 감소시키고 카운트가 0이면 메모리를 해제합니다.
또는 가치 의미 및 딥 복사 동작을 구현할 수 있습니다 .
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment
Person copy(other);
swap(copy); // assume swap() exchanges contents of *this and copy
return *this;
}
복사 할당 연산자의 구현은 기존 버퍼를 해제해야 할 필요가 있기 때문에 복잡합니다. 복사 및 스왑 기법은 새로운 버퍼를 보유하는 임시 객체를 생성합니다. 내용 스와핑 *this
과 copy
의 소유권을 제공하는하는 copy
원 버퍼. copy
파기는, 함수가 돌려주는대로, *this
의해 소유되고 있던 버퍼를 해방합니다.
자체 할당 보호
복사 할당 연산자를 쓸 때, 자기 할당의 경우에 작동 할 수있을 것이 매우 중요합니다. 즉, 다음을 허용해야합니다.
SomeType t = ...;
t = t;
자기 할당은 대개 그런 명백한 방법으로 발생하지 않습니다. 일반적으로 다양한 코드 시스템을 통한 순환 경로를 통해 발생합니다. 할당의 위치는 단순히 두 개의 Person
포인터 또는 참조를 가지며 이들이 동일한 객체라는 것을 전혀 모릅니다.
작성한 모든 대입 연산자는 이것을 고려해야합니다.
이렇게하는 일반적인 방법은 할당 로직을 다음과 같은 조건으로 모두 래핑하는 것입니다.
SomeType &operator=(const SomeType &other)
{
if(this != &other)
{
//Do assignment logic.
}
return *this;
}
참고 : 자체 할당에 대해 생각하고 코드가 올바르게 작동하는지 확인하는 것이 중요합니다. 그러나 자체 할당은 매우 드물게 발생하며 정상적인 경우를 실제로 피할 수 없도록 최적화합니다. 일반적인 경우가 훨씬 더 일반적이므로 자체 할당을 위해 비관적으로 사용하면 코드 효율성이 저하 될 수 있으므로주의해야합니다.
예를 들어, 할당 연산자를 구현하는 일반적인 기술은 copy and swap idiom
입니다. 이 기술의 정상적인 구현은 자체 할당을 테스트하는 것을 귀찮게하지 않습니다 (사본이 만들어지기 때문에 자체 할당이 비싸지 만). 그 이유는 정상적인 경우의 비관이 훨씬 더 많은 비용이 드는 것으로 나타 났기 때문입니다.
이동 할당 연산자도 자체 할당으로부터 보호되어야합니다. 그러나, 많은 그러한 연산자에 대한 논리는 std::swap
기반으로합니다. std::swap
은 동일한 메모리와의 스왑을 잘 처리 할 수 있습니다. 따라서 이동 할당 논리가 일련의 스왑 연산에 지나지 않는다면 자체 할당 보호가 필요하지 않습니다.
그렇지 않은 경우 위와 유사한 조치를 취해야 합니다 .