KoreanFoodie's Study

Effective Modern C++ | 항목 17 : 특수 멤버 함수들의 자동 작성 조건을 숙지하라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 17 : 특수 멤버 함수들의 자동 작성 조건을 숙지하라

GoldGiver 2022. 10. 26. 09:55

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 17 : 특수 멤버 함수들의 자동 작성 조건을 숙지하라

핵심 :

1. 컴파일러가 스스로 작성할 수 있는 멤버 함수들, 즉 기본 생성자와 소멸자, 복사 연산들, 이동 연산들을 가리켜 특수 멤버 함수라고 한다.
2. 이동 연산들은 이동 연산들이나 복사 연산들, 소멸자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성된다.
3. 복사 생성자는 복사 생성자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성되며, 만일 이동 연산이 하나라도 선언되어 있으면 삭제된다. 복사 배정 연산자는 복사 배정 연산자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성되며, 만일 이동 연산이 하나라도 선언되어 있으면 삭제된다. 소멸자가 명시적으로 선언된 클래스에서 복사 연산들이 자동으로 작성되는 기능은 비권장이다.
4. 멤버 함수 템플릿 때문에 특수 멤버 함수의 자동 작성이 금지되는 경우는 전혀 없다.


C++ 에서 기본 생성자와 소멸자, 복사 연산들, 이동 연산들을 가리켜 특수 멤버 함수(special member function)라고 한다. 특수 멤버 함수들은 암묵적으로 public 이자 inline 이며, 가상 소멸자가 있는 기반 클래스를 상속하는 파생 클래스의 소멸자를 제외하고는 비가상(non-virtual) 이다(기반 클래스의 소멸자가 가상이면 파생 클래스의 소멸자도 가상).
두 복사 연산은 독립적이어서, 복사 생성자를 선언한다고 복사 배정 연산자를 컴파일러가 만들지 않는 상황은 일어나지 않는다. 하지만 두 이동 연산은 독립적이지 않다. 왜냐하면 프로그래머가 어떤 클래스에 대해 이동 생성자를 선언했다면, 그것은 컴파일러가 기본적인 멤버별 이동 생성이 그 클래스에 적합하지 않아서 다른 방식으로 구현했다고 컴파일러가 받아들이기 때문이다.
더 나아가서, 복사 연산(생성 또는 배정)을 하나라도 명시적으로 선언한 클래스에 대해서는 이동 연산들이 작성되지 않는다. 이유는 앞서 설명했던 것과 동일한데, 이는 이동 연산(생성 또는 배정)을 하나라도 명시적으로 선언했을 경우에도 마찬가지로 적용된다(즉, 복사 연산들이 자동으로 생기지 않는다는 것이다).
클래스를 만들 때 3의 법칙(Rule of Three) 를 고려할 필요가 있는데, 이는 만일 복사 생성자와 복사 배정 연산자, 소멸자 중 하나라도 선언했다면 나머지 둘도(즉, 셋 다) 선언해야 한다는 것이다! C++ 은 사용자 선언 소멸자가 있는 클래스에 대해서는 이동 연산들을 작성하지 않는다.
정리하자면, 클래스에 대한 이동 연산들은 다음 세 조건이 모두 만족될 때에만, 그리고 필요할 때에만, 자동으로 작성된다.

  • 클래스에 그 어떤 복사 연산도 선언되어 있지 않다
  • 클래스에 그 어떤 이동 연산도 선언되어 있지 않다
  • 클래스에 소멸자가 선언되어 있지 않다

만약 복사/이동 연산자나 소멸자를 명시적으로 정의하면서, 기본적으로 만들어지는 다른 연산들을 사용하고 싶다면, 다음과 같이 하면 된다.

class Widget {
public:
  // 사용자 선언 소멸자
  virtual ~Widet();

  // 기본 복사 생성자의 기본 행동 사용
  Widget(const Widget& other) = default;

  // 기본 복사 배정(대입) 연산자 기본 행동 사용
  Widget& operator=(const Widget& other) = default;

  // 이동 연산들 지원
  Widget(Widget&& other) = default;
  Widget&& operator=(Widget&& other) = default;
};

기존 클래스에서 소멸자를 하나 추가한다고 가정해 보자. 소멸자를 하나 추가한다고 컴파일이 안되는 것은 아니겠지만, 기본으로 생성되던 이동 연산들이 만들어지지 않아 대입 및 생성이 무조건 복사의 형태로 이루어지게 될 수도 있다. 이럴 경우, 심각한 성능 문제가 발생할 수 있으므로, '= default' 를 붙여 주는 것은 좋은 관례일 수 있다!
특수 멤버 함수들을 관장하는 C++11 의 규칙들을 정리하면 다음과 같다.

  • 기본 생성자 : 사용자 선언 생성자가 없으면 자동으로 생성.
  • 소멸자 : 소멸자는 기본적으로 noexcept 임. 기반 클래스 소멸자가 가상일 때만 가상임.
  • 복사 생성자 : 사용자 복사 생성자나 이동 연산이 있으면 비활성화.
  • 복사 배정 연산자 : 사용자 복사 배정 연산자나 이동 연산이 있으면 비활성화
  • 이동 생성자 / 이동 배정 연산자 : 사용자 선언 복사 연산들과 이동 연산들, 소멸자가 없을 때에만 자동으로 작성


멤버 함수 템플릿이 존재한다고 특수 멤버 함수의 자동 작성이 비활성화 되지는 않는다. 다음을 보자.

class Widget {
public:
  // 그 어떤 것으로도 Widget 을 생성
  template<typename T>
  Widget(const Widget& other);

  // 그 어떤 것으로도 Widget 을 배정
  template<typename T>
  Widget operator=(const Widget& other);
};

위에서 T 에 Widget 이 들어가면 기본 복사 생성자나 복사 배정 연산자의 서명과 일치하는 함수들로 인스턴스화될 수 있지만, 그래도 컴파일러는 여전히 Widget 의 복사 연산들과 이동 연산들을 작성한다. 이와 관련해서 생길 수 있는 문제는 항목 26 에서 보게 될 것이다!

Comments