Abseil Tip 143 C++11 삭제된 함수 (= delete)

주간 팁 #143: C++11 삭제된 함수 (= delete)

원래 게시일: 2018년 3월 2일

작성자: 레오나르드 모세스쿠

업데이트: 2020-04-06

퀵 링크: abseil.io/tips/143

소개

인터페이스는 일반적으로 호출할 수 있는 연산의 집합을 정의합니다. 그러나 때때로 우리는 반대로 사용하지 말아야 할 연산 집합을 명시적으로 정의하고 싶을 때가 있습니다. 예를 들어, 복사 생성자와 복사 대입 연산자를 비활성화하는 것은 특정 타입에 대한 복사 동작을 제한하는 일반적인 방법입니다.

언어는 이러한 제한을 적용하기 위한 여러 가지 방법을 제공합니다(각 방법을 곧 살펴보겠습니다):

  1. 런타임 검사로 구성된 더미 정의를 제공.
  2. 함수 접근을 제한(Protected/Private)을 통해 함수 접근을 불가능하게 만듦.
  3. 함수 선언만 하고 의도적으로 정의를 생략.
  4. C++11부터: 함수 정의를 명시적으로 “삭제”로 정의.

C++11 이전의 기법들은 런타임 검사(#1)부터 컴파일 타임(#2) 또는 링크 타임(#3) 진단까지 다양합니다. 이러한 기법들은 모두 사용되었지만 완벽하지는 않습니다. 예를 들어, 런타임 검사는 대부분의 상황에서 정적 제약을 적용하려는 경우에는 이상적이지 않으며, 링크 타임 검사는 빌드 과정에서 진단이 너무 늦게 발생합니다. 또한 링크 타임 진단은 반드시 발생하는 것이 아니며, ODR(One Definition Rule) 위반이 될 수도 있습니다. 게다가 생성되는 오류 메시지는 종종 개발자가 이해하기 어렵습니다.

컴파일 타임 검사도 더 나은 방법이지만 여전히 완벽하지 않습니다. 이는 멤버 함수에만 적용되며 접근성 제한을 기반으로 하므로, 오류 메시지가 길고 실수하기 쉽고, 실제 오류 메시지가 접근 제한과 관련된 내용으로 잘못 표시될 수 있습니다.

복사 동작을 비활성화하려면 다음과 같은 코드로 나타낼 수 있습니다:

class MyType {
 private:
  MyType(const MyType&);  // 정의되지 않음
  MyType& operator=(const MyType&);  // 정의되지 않음
  // ...
};

이러한 방식으로 클래스를 매번 처리하는 것은 지루하므로 개발자들은 보통 이를 다음과 같은 방식으로 패키징합니다:

“믹스인” 방식 ([boost::noncopyable][boost], [비복사 가능한 믹스인][mixin])

class MyType : private NoCopySemantics {
  ...
};

매크로 방식

class MyType {
 private:
  DISALLOW_COPY_AND_ASSIGN(MyType);
};

C++11 삭제된 함수

C++11은 =delete라는 새로운 문법을 통해 더 나은 해결책을 제공하였습니다. 이는 함수 정의를 명시적으로 “삭제”로 정의하는 방법입니다:

void foo() = delete;

이 문법은 직관적이며 중요한 차이점이 있습니다:

  1. 함수는 멤버 함수에 국한되지 않고, 비멤버 함수도 삭제할 수 있습니다. (=default는 특별 멤버 함수에만 적용됩니다.)
  2. 함수는 첫 번째 선언에서만 삭제해야 합니다. (=default와 달리 여러 번 삭제할 수 없습니다.)

중요한 점은 =delete가 함수의 정의라는 것입니다. 즉, 선언을 제거하거나 숨기는 것이 아닙니다. 삭제된 함수는 여전히 이름 검색 및 오버로드 해결에 참여합니다. 이는 일종의 “핫한” 정의로, “건드리지 말라!”라는 메시지를 전달합니다.

삭제된 함수를 사용하려고 시도하면 컴파일 타임 오류가 발생하며, 이는 C++11 이전의 기법보다 훨씬 명확한 진단 메시지를 제공합니다.

class MyType {
 public:
  // 기본 생성자 비활성화
  MyType() = delete;

  // 복사(및 이동) 연산자 비활성화
  MyType(const MyType&) = delete;
  MyType& operator=(const MyType&) = delete;

  //...
};
// error: 'MyType'의 삭제된 생성자를 호출하려고 함
// note: 'MyType'은 여기서 명시적으로 삭제된 상태입니다
//   MyType() = delete;
MyType x;

void foo(const MyType& val) {
  // error: 'MyType'의 삭제된 생성자를 호출하려고 함
  // note: 'MyType'은 여기서 명시적으로 삭제된 상태입니다
  //   MyType(const MyType&) = delete;
  MyType copy = val;
}

참고: 복사 연산자를 명시적으로 삭제하면 이동 연산자도 비활성화됩니다(사용자 정의 복사 연산자가 있으면 이동 연산자는 암시적으로 선언되지 않기 때문). 이동만 허용하려면 =default를 사용하여 이동 연산자를 “되살릴” 수 있습니다.

MyType(MyType&&) = default;
MyType& operator=(MyType&&) = default;

기타 사용 사례

위의 예제는 복사 연산자에 관한 것이지만, 실제로는 모든 함수(멤버 함수 및 비멤버 함수)를 삭제할 수 있습니다.

삭제된 함수는 오버로드 해결에 참여하므로 의도하지 않은 사용을 잡아낼 수 있습니다. 예를 들어, 다음과 같은 print 함수가 있을 때:

void print(int value);
void print(absl::string_view str);

print('x')를 호출하면 ‘x’의 정수 값이 출력됩니다. 그러나 개발자는 print("x")를 의도했을 수 있습니다. 이를 다음과 같이 수정할 수 있습니다:

void print(int value);
void print(const char* str);
// 문자 리터럴 ':' 대신 문자열 리터럴 ':'를 사용하세요.
void print(char) = delete;

=delete는 함수 호출에만 영향을 미치는 것이 아닙니다. 삭제된 함수의 주소를 취하려고 해도 컴파일 오류가 발생합니다:

void (*pfn1)(int) = &print;  // OK
void (*pfn2)(char) = &print; // error: 삭제된 함수 사용 시도

이 예제는 실제 애플리케이션에서 발췌한 것입니다: [absl::StrCat()][strcat]. 삭제된 함수는 특정 인터페이스의 일부를 제한해야 할 때 유용합니다.

소멸자를 삭제하는 것은 이를 private으로 만드는 것보다 더 엄격합니다(하지만 이는 강력한 제약이므로 원하지 않는 제약을 도입할 수 있습니다).

// 매우 제한된 타입:
//   1. 동적 저장소만 사용.
//   2. 영원히 살아있음(소멸 불가).
//   3. 멤버나 기반 클래스가 될 수 없음.
class ImmortalHeap {
 public:
  ~ImmortalHeap() = delete;
  //...
};

또 다른 예제는 배열 객체 할당을 제한하려는 경우입니다([실제 사례][crashpad]):

// T[] 할당 불가.
class NoHeapArraysPlease {
 public:
  void* operator new[](std::size_t) = delete;
  void operator delete[](void*) = delete;
};

auto p = new NoHeapArraysPlease;  // OK

// error: 삭제된 함수 'operator new[]' 호출
// note: 후보 함수가 명시적으로 삭제됨
//   void* operator new[](std::size_t) = delete;
auto pa = new NoHeapArraysPlease[10];

요약

=delete는 참조되어서는 안 되는 인터페이스의 부분을 명시적으로 표현하는 방법을 제공합니다. 이전의 기법보다 훨씬 더 나은 진단을 가능하게 해주며, 컴파일러가 생성하는 코드도 삭제된 함수를 참조할 수 없습니다. 보다 정교한 접근 제어가 필요하면 접근 지정자나 더 복잡한 기법(예: )이 더 적합합니다.페이스의 일부이므로 다른 인터페이스 부분과 동일한 접근 지정자를 사용하는 것이 좋습니다. 일반적으로 이를 공개(public)로 설정하는 것이 최선입니다. 실제로 이는 가장 나은 진단을 제공합니다(private와 =delete는 잘 맞지 않습니다).

참고: 이 팁은 많은 사람들의 기여와 피드백을 기반으로 작성되었습니다. Mark Mentovai, James Dennett, Bruce Dawson, Yitzhak Mandelbaum에게 특별히 감사드립니다.

참고 자료

  • [TotW