[EMC++] Item 17. 특수 멤버 함수들의 자동 작성 조건을 숙지해라
Item 17. 특수 멤버 함수들의 자동 작성 조건을 숙지해라
특수 멤버 함수(special member function)
C++에서 클래스를 구현하면 내가 따로 선언해주지 않아도 컴파일러가 특수 멤버 함수를 만들어 준다.
C++98에서는 특수 멤버 함수로 기본 생성자, 소멸자, 복사 생성자, 복사 대입 연산자가 만들어지고 C++11에서는 이동 생성자, 이동 대입 연산자가 추가되었다.
class Widget{
public:
Widget(); // 기본 생성자
~Widget(); // 기본 소멸자
Widget(const Widget& w); // 복사 생성자
Widget& operator=(const Widget& w); // 복사 대입 연산자
Widget(Widget&& w); // 이동 생성자
Widget& operator=(Widget&& w); // 이동 대입 연산자
};
즉, 조건만 맞으면 컴파일러는 위 6가지 함수를 선언하지 않아도 자동으로 생성해준다.
작성된 특수 멤버 함수는 public
함수이며 inline
함수이다.
또한 부모 클래스를 상속받는 자식 클래스의 소멸자만 virtual
함수이고 나머지는 virtual
함수가 아니다.
특수 멤버 함수들의 자동 작성 조건
C++11의 특수 멤버 함수들의 자동 작성 조건은 다음과 같다.
이러한 규칙을 Rule of three/five/zero 라고 한다.
기본 생성자(default constructor)
클래스에 사용자가 직접 선언한 생성자가 없는 경우에만 자동으로 생성된다.
소멸자(destructor)
클래스에 사용자가 직접 선언한 소멸자가 없는 경우에만 자동으로 생성된다.
다만 C++11에서는 소멸자가 기본적으로 noexcept
이다.
또한 부모 클래스 소멸자가 virtual
이어야 자동으로 생성되는 소멸자도 virtual
이다.
복사 생성자(copy constructor)와 복사 대입 연산자(copy assignment operator)
컴파일러가 자동으로 생성하는 복사 생성자와 복사 대입 연산자는 static
이 아닌 멤버 변수를 모두 복사한다. 만약 상속된 클래스라면 부모 클래스에 해당하는 부분도 복사한다.
클래스에 사용자가 직접 선언하지 않은 경우에만 복사 생성자와 복사 대입 연산자가 자동으로 생성된다. (클래스에 이동 연산이 하나라도 선언되어 있으면 삭제(비활성화)된다.)
복사 연산은 서로 독립적이다.
복사 생성자를 선언하고 복사 대입 연산자를 선언하지 않은 경우 컴파일러는 자동으로 복사 대입 연산자를 생성한다. (반대도 마찬가지이다.)
사용자가 직접 선언한 복사 대입 연산자나 소멸자가 있는 경우에는 자동으로 생성되는 기능은 비권장(deprecated)된다.
이는 Rule of Three 때문인데 복사 생성자, 복사 대입 연산자, 소멸자 중 하나라도 선언했으면 나머지도 선언해야 한다는 규칙이다. (대부분 깊은 복사가 필요하면 메모리 관리도 필요하기 때문이다.)
하지만 이를 강제하면 C++98 시절부터 사용하던 기존 코드에 문제가 생길 수도 있어 비권장(deprecated)하는 것이다.
이동 생성자(move constructor)와 이동 대입 연산자(move assignment operator)
컴파일러가 자동으로 생성하는 이동 생성자와 이동 대입 연산자는 static
이 아닌 멤버 변수를 모두 이동한다. 만약 상속된 클래스라면 부모 클래스에 해당하는 부분도 이동한다. (이동할 수 없는 타입들은 복사한다.)
이동 생성자와 이동 대입 연산자는 자동 생성 조건이 매우 까다롭다.
클래스에 사용자가 직접 선언한 복사 연산, 이동 연산, 소멸자가 없을 때만 이동 생성자와 이동 대입 연산자가 자동으로 생성된다. (하나라도 있으면 자동으로 생성되지 않는다.)
이동 연산은 독립적이지 않다.
이동 생성자와 이동 대입 연산자 중 하나를 선언하면 컴파일러는 다른 하나를 선언하지 않는다.
default
C++11에서는 default
키워드를 통해 컴파일러가 자동으로 작성해주는 특수 멤버 함수를 사용하겠다고 명시적으로 표현할 수 있다.
class Widget{
public:
Widget() = default;
~Widget() = default;
Widget(const Widget& w) = default;
Widget& operator=(const Widget& w) = default;
Widget(Widget&& w) = default;
Widget& operator=(Widget&& w) = default;
};
default
를 사용하면 특수 멤버 함수가 자동으로 작성이 안되는 상황에서도 특수 멤버 함수를 사용할 수 있다.
또한 특수 멤버 함수를 사용하겠다는 사용자의 의도를 명확하게 할 수 있으며 미묘한 버그를 피할 수 있다.
다음과 같은 문자열을 찾는 자료구조를 가지고 있는 클래스가 있다고 가정해보자.
class StringTable{
public:
StringTable() {}
private:
std::map<int, std::string> values;
};
해당 클래스는 특수 멤버 함수의 자동 작성 규칙에 따라 소멸자, 복사/이동 생성자, 복사/이동 대입 연산자가 자동으로 생성된다.
굳이 해당 함수들을 작성하지 않아도 편리하게 사용할 수 있다.
class StringTable{
public:
StringTable() { makeLogEntry("Creating object"); }
~StringTable() { makeLogEntry("Destroting object"); }
private:
std::map<int, std::string> values;
};
하지만 다음과 같이 로그를 찍기 위해 소멸자를 작성해리면 특수 멤버 함수의 자동 작성 규칙에 따라 복사 생성자, 복사 대입 연산자만 자동으로 생성되고 이동 연산은 자동으로 생성되지 않는다.
따라서 이동이 필요한 상황에서도 복사를 하여 성능이 떨어지는 문제가 생길 수 있다.
이때 default
를 사용해 명시적으로 이동 연산을 정의하여 문제를 해결할 수 있다.
class StringTable{
public:
StringTable() { makeLogEntry("Creating object"); }
~StringTable() { makeLogEntry("Destroting object"); }
StringTable(StringTable&& st) = default;
StringTable& operator=(StringTable&& st) = default;
private:
std::map<int, std::string> values;
};
템플릿 멤버 함수 사용 시 자동 작성 조건
템플릿 멤버 함수를 사용한다고 특수 멤버 함수의 자동 작성 조건이 삭제(비활성화)되지 않는다.
class Widget {
public:
template <typename T>
Widget(const T& rhs);
template <typename T>
Widget& operator=(const T& rhs);
};
만약 T
가 Widget
일 경우 해당 템플릿 멤버 함수는 복사 생성자, 복사 대입 연산자와 같은 모양의 함수를 인스턴스화한다.
하지만 이와 상관없이 컴파일러는 복사 생성자, 복사 대입 연산자를 자동 생성한다.
자세한 이유는 Item 26에서 다루겠다.