Abseil Tip 36 새로운 Join API


title: “주간 팁 #131: 특별 멤버 함수와 = default” layout: tips sidenav: side-nav-tips.html published: true permalink: tips/131 type: markdown order: “131” —

원래 2017-03-24에 totw/131로 게시됨

작성자: James Dennett (jdennett@google.com)

C++는 초기부터 특별 멤버 함수라 불리는 몇 가지 기능을 지원했습니다: 기본 생성자, 소멸자, 복사 생성자 및 복사 할당 연산자입니다. C++11은 여기에 이동 생성자와 이동 할당 연산자를 추가하고, =default=delete라는 문법을 통해 이러한 기본값을 선언 및 정의하는 시점을 제어할 수 있게 했습니다.

=default는 무엇을 하고, 왜 사용해야 할까요?

=default를 사용하는 것은 컴파일러에게 “이 특별 멤버 함수에 대해 일반적으로 하던 대로 처리해라”고 지시하는 것입니다. 그렇다면 이를 수동으로 구현하거나 컴파일러가 선언하도록 내버려 두는 대신 사용하는 이유는 무엇일까요?

  • 접근 수준을 변경할 수 있습니다(예: 생성자를 public 대신 protected로 설정). 또는 가상 소멸자를 만들거나 억제된 함수(예: 다른 사용자 정의 생성자가 있는 클래스의 기본 생성자)를 복구하면서 컴파일러가 함수를 생성하도록 할 수 있습니다.
  • 컴파일러 정의 복사 및 이동 연산은 멤버가 추가되거나 제거될 때마다 유지 보수가 필요 없으며, 멤버를 복사/이동하는 것이 충분한 경우가 많습니다.
  • 컴파일러가 제공하는 특별 멤버 함수는 사소한(trivial) 경우가 있을 수 있습니다(호출되는 모든 연산이 사소한 경우). 이는 더 빠르고 안전할 수 있습니다.
  • 기본 생성자가 있는 타입은 집합체(aggregate)가 될 수 있으며, 집합체 초기화를 지원합니다. 반면 사용자 정의 생성자가 있는 경우는 그렇지 않습니다.
  • 명시적으로 선언된 기본 멤버는 결과 함수의 의미를 문서화하는 데 도움이 됩니다.
  • 클래스 템플릿에서 =default는 기본 타입이 해당 연산을 제공하는지 여부에 따라 조건부로 연산을 선언하는 간단한 방법이 됩니다.

특별 멤버 함수의 초기 선언에 =default를 사용하는 경우, 컴파일러는 해당 함수에 대한 인라인 정의를 생성할 수 있는지 확인합니다. 가능하다면 그렇게 처리합니다. 그렇지 않다면 해당 함수는 실제로 삭제된(deleted) 것으로 선언됩니다(=delete를 쓴 것처럼). 이는 클래스 템플릿을 정의하거나 클래스 래핑 시 투명하게 처리하는 데 유용하지만, 독자들에게는 놀라울 수 있습니다.

특정 함수의 초기 선언에 =default를 사용하거나 컴파일러가 사용자 선언되지 않은 특별 멤버 함수를 선언하는 경우 적절한 noexcept 명세가 유추되어 더 빠른 코드 생성이 가능해질 수 있습니다.

어떻게 동작하나요?

C++11 이전에는 기본 생성자가 필요하면서 다른 생성자도 이미 존재하는 경우 다음과 같이 작성했습니다:

class A {
 public:
  A() {}  // 사용자 제공, 비사소한(non-trivial) 생성자는 A를 집합체가 아니게 만듭니다.
};

C++11 이후로는 더 많은 선택지가 있습니다.

class C {
 public:
  C() = default;  // 오해를 불러일으킬 수 있음: C는 삭제된 기본 생성자를 가집니다.
 private:
  const int i;  // const이므로 항상 초기화되어야 합니다.
};

class D {
 public:
  D() = default;  // 예상 가능하지만 명시적이지 않음: D는 기본 생성자를 가짐
 private:
  std::unique_ptr<int> p;  // std::unique_ptr은 기본 생성자를 가짐
};

class C와 같은 코드는 작성하지 않는 것이 좋습니다. 템플릿이 아닌 경우 =default를 사용하는 것은 해당 클래스가 해당 연산을 지원할 의도가 있을 때만 사용하세요(그리고 이를 테스트하세요). clang-tidy는 이를 검사하는 도구를 제공합니다.

=default를 특별 멤버 함수의 첫 번째 선언 이후에 사용하는 경우(즉, 클래스 외부에서), 더 단순한 의미를 가집니다: 컴파일러에게 해당 함수를 정의하도록 지시하며, 정의할 수 없는 경우 오류를 발생시킵니다. 클래스 외부에서 =default를 사용하면 해당 함수는 더 이상 사소하지 않습니다. 사소한 여부는 첫 번째 선언에서 결정되기 때문입니다(그래야 모든 클라이언트가 연산이 사소한지 여부를 일치하게 이해함).

클래스가 집합체일 필요가 없고 생성자가 사소할 필요도 없다면, 아래 EF 예시처럼 생성자를 클래스 정의 외부에서 기본값으로 설정하는 것이 종종 좋은 선택입니다. 이는 독자들에게 명확하며 컴파일러가 이를 확인합니다. 기본 생성자나 소멸자를 기본값으로 설정하는 특별한 경우에는 {}를 쓸 수 있지만, 다른 기본 연산의 경우 컴파일러 생성 구현은 덜 간단하므로 일관성을 위해 모든 경우에 =default를 쓰는 것이 좋습니다.

class E {
 public:
  E();  // 기본 생성자가 있음을 약속하지만...
 private:
  const int i;  // const이므로 항상 초기화되어야 합니다.
};
inline E::E() = default;  // 컴파일 오류 발생: `i`를 초기화하지 않음

class F {
 public:
  F();  // 기본 생성자가 있음을 약속함
 private:
  std::unique_ptr<int> p;  // std::unique_ptr은 기본 생성자를 가짐
};
inline F::F() = default;  // 예상대로 동작

권장 사항

수동으로 구현한 코드가 {}로 작성한 것과 동일하더라도 =default를 선호하세요. 초기 선언에서 =default를 생략하고 별도의 기본값 구현을 제공하는 것도 선택 사항입니다.

이동 연산을 기본값으로 설정할 때는 신중해야 합니다. 이동된 객체는 여전히 해당 타입의 불변 조건을 충족해야 하며, 기본 구현은 필드 간의 관계를 유지하지 않을 가능성이 높습니다.

템플릿이 아닌 경우, =default가 구현을 제공하지 않을 경우에는 대신 =delete를 작성하세요.