- 컴파일러가 스스로 작성할 수 있는 멤버 함수들, 즉 기본 생성자와 소멸자, 복사 연산자들, 이동 연산자들을 가리켜 특수 멤버 함수라고 부른다.
1. 기본 생성자 : C++98의 규칙들과 같다. 클래스에 사용자 선언 생성자가 없는 경우에만 자동으로 작성된다.
2. 소멸자 : C++98의 규칙들과 본질적으로 같다. 유일한 차이점은 소멸자가 기본적으로 noexcept라는 점이다. C++98처럼 기본적으로 작성되는 소멸자는 오직 기반 클래스 소멸자가 가상일 때에만 가상이다.
3. 복사 생성자 : 실행시점 행동은 C++98의 것과 같다. 즉, 비정적 데이터 멤버들을 멤버별로 복사 생성한다. 클래스에 사용자 선언 복사 생성자가 없을 때에만 자동으로 작성된다. 클래스에 이동 연산이 하나라도 선언되어 있으면 삭제(비활성화)된다. 사용자 선언 복사 대입 연산자나 소멸자가 있는 클래스에서 이 함수가 자동 작성되는 기능은 비권장이다.
4. 복사 대입 연산자 : 실행시점 행동은 C++98의 것과 같다. 즉, 비정적 데이터 멤버들을 멤버별로 복사 대입한다. 클래스에 사용자 선언 복사 대입 연산자가 없을 때에만 자동으로 작성된다. 클래스에 이동 연산이 하나라도 선언되어 있으면 삭제(비활성화)된다. 사용자 선언 복사 생성자나 소멸자가 있는 클래스에서 이 함수가 자동 작성되는 기능은 비권장이다.
5. 이동 생성자와 이동 대입 연산자 : 각각 비정적 데이터 멤버의 멤버별 이동을 수행한다. 클래스에 사용자 선언 복사 연산들과 이동 연산들, 소멸자가 없을 때에만 자동으로 작성된다.
C++11에서는 명시적으로 선언된 특수 함수가 기본행동을 사용하겠다는 의사를 "=default"를 이용해서 명시적으로 표현할 수 있다.
소멸자나 복사 연산 중 하나를 선언하면 복사 연산들이 자동으로 작성된다는 점에 의존하는 것이 있다면, 그러한 의존성이 사라지도록 클래스를 업그레이드하는 것이 바람직하다.
컴파일러가 작성한 함수들의 행동이 정확하다면, 그러한 업그레이드는 쉬운 일이다.
C++11에서는 기본 행동을 사용하겠다는 의사를 " = default"를 이용해서 명시적으로 표현할 수 있기 때문이다.
class Widget {
public:
~Widget(void); // 사용자 선언 소멸자
Widget(const Widget&) = default; // 기본 복사 생성자
Widget& operator=(const Widget&) = default; // 기본 복사 대입
};
이러한 접근방식은 다형적 기반 클래스, 즉 파생 클래스 객체들을 조작하는 데 쓰이는 인터페이스를 정의하는 클래스에
유용한 경우가 많다.
대체로 다형적 기반 클래스에는 가상 소멸자가 있다.
이미 가상으로 선언된 소멸자를 상속하는 것이 아닌 한, 소멸자가 가상이 되게 만드는 유일한 방법은 명시적으로 virtual로 선언하는 것뿐이다.
가상으로 만드는것 외에는 기본 구현을 그대로 사용하고 싶다고 하자.
다음과 같이 할 수 있다.
class Base {
public:
virtual ~Base(void) = default; // 소멸자를 가상으로
Base(Base&&) = default; // 이동 지원
Base& operator=(Base&&) = default;
Base(const Base&) = default; // 복사 지원
Base& operator=(const Base&) = default;
};
이렇게 기본 구현을 사용하겠다고 명시적으로 선언하는것은, 타자량이 좀 늘더라도 이로울 때가 많다.
정수 ID를 통해서 문자열 값을 빠르게 조회할 수 있는 자료구조를 나타내는 클래스가 있다고 하자.
class StringTable {
public:
StringTable(void) {}
// 삽입, 삭제, 조회 등을 위한 함수들은 있지만 복사/이동/소멸자 기능성은 없음
private:
std::map<int, std::string> values;
};
이 클래스가 복사 연산들과 이동 연산들, 그리고 소멸자를 전혀 선언하지 않는다면, 그리고 그런 함수들을 사용하는 클라이언트 코드가 있다면, 컴파일러는 해당 함수들을 자동으로 만들어 준다.
그러나, 나중에 이런 객체들의 기본 생성과 소멸을 기록하는 것이 유용하겠다는 생각이 들었다고 하자.
class StringTable {
public:
StringTable(void)
{
makeLogEntry("Create StringTable object");
}
~StringTable(void)
{
makeLogEntry("Destroying StringTable object");
}
private:
std::map<int, std::string> values; // 이전과 동일
};
이제 컴파일러는 복사 연산들은 자동으로 작성하지만, 이동 연산들은 자동으로 작성하지 않는다.
문제는 이동 연산이 없어도 무사히 컴파일되며, 실행도 아무런 문제 없이 된다.
때문에 이동 연산을 명시적으로 선언하지 않는 실수를 하기 쉽다.
문제는, 이동 연산이 일어나야 할 시점에 대신 복사 연산이 일어나서 효율이 심각하게 떨어질 수 있다.
- 이동 연산들은 이동 연산들이나 복사 연산들, 소멸자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성된다.
복사 연산들이나 소멸자가 명시적으로 선언되었다는 것은, 자동으로 작성된 복사 연산들과 소멸자가 적합하지 않다는 이야기인데, 이 경우 자동으로 작성된 이동 연산도 적합하지 않을 가능성이 크다.
자동으로 작성된 이동 연산들은 비정적 데이터 멤버들을 멤버별로 이동을 수행하는데, 만일 어떤 데이터 멤버가 이동 연산을 지원하지 않는다면 그 데이터 멤버는 이동 연산 대신 복사 연산이 일어난다. 기반 클래스가 이동 연산을 지원하지 않는 경우에도 마찬가지로, 기반 클래스에 대해서 이동 연산 대신 복사 연산이 일어난다.
- 복사 생성자는 복사 생성자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성되며, 만일 이동 연산이 하나라도 선언되어 있으면 삭제된다. 복사 대입 연산자는 복사 대입 연산자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성되며, 만일 이동 연산이 하나라도 선언되어 있으면 삭제된다. 소멸자가 명시적으로 선언된 클래스에서 복사 연산들이 자동 작성되는 기능은 비권장이다.
이동 연산들이 명시적으로 선언되었다는 것은, 자동으로 작성된 이동 연산들이 적합하지 않다는 이야기인데, 이 경우 자동으로 작성된 복사 연산들도 적합하지 않을 가능성이 크다.
복사 생성자(또는 복사 대입 연산자)가 복사 대입 연산자(또는 복사 생성자)나 소멸자가 명시적으로 선언되었을때도 자동으로 작성되는 것은, 그렇지 않도록 복사 연산들이 작성되는 조건을 제한하면 기존 코드가 너무 많이 깨질 것이라는 판단 때문이다.
- 멤버 함수 템플릿 때문에 특수 멤버 함수의 자동 작성이 금지되는 경우는 전혀 없다.
멤버 함수 템플릿이 존재하면 특수 멤버 함수의 자동 작성이 비활성화된다는 규칙은 없음을 주목하기 바란다.
다음과 같은 Widget 클래스가 있다고 하자.
class Widget {
public:
template <typename T> // 그 어떤 것으로도
Widget(const T& rhs); // Widget을 생성
template <typename T> // 그 어떤 것으로도
Widget& operator=(const Widget& rhs); // Widget을 대입
};
이 템플릿들이 복사 생성자나 복사 대입 연산자의 시그니처와 일치하는 함수들로 인스턴스화될 가능성이 있지만, 그래도 컴파일러는 여전히 Widget의 복사 연산들과 이동 연산들을 작성한다(조건이 만족되었다면).
'PROGRAMMING > Effective Modern C++' 카테고리의 다른 글
19. 소유권 공유 자원의 관리에는 std::shared_ptr을 사용하라. (0) | 2021.03.09 |
---|---|
18. 소유권 독점 자원의 관리에는 std::unique_ptr을 사용하라. (0) | 2021.03.09 |
16. const 멤버 함수를 스레드에 안전하게 작성하라. (0) | 2021.03.09 |
15. 가능하면 항상 constexpr을 사용하라. (0) | 2020.12.27 |
14. 예외를 방출하지 않을 함수는 noexcept로 선언하라. (0) | 2020.12.27 |
댓글