KoreanFoodie's Study

Effective Modern C++ | 항목 11 : 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 11 : 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하라

GoldGiver 2022. 10. 26. 09:53

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

항목 11 : 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하라

핵심 :

1. 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하라.
2. 비멤버 함수와 템플릿 인스턴스를 비롯한 그 어떤 함수도 삭제할 수 있다.


다른 개발자에게 제공할 코드를 작성할 때, 그 개발자가 코드의 특정 함수를 호출하지 못하게 하는 가장 흔한 방법은 그냥 그 함수를 선언하지 않는 것이다. 하지만 컴파일러가 때로는 함수를 자동으로 생성하는 경우가 있다. C++ 98 에서는 private 으로 선언 후, 정의를 하지 않는 방식으로 이를 방지하기도 한다. 실제로 입력 스트림 객체는 다음과 같이 구현되어 있다.

template<class charT, class traits = char_traits<charT>>
class basic_ios : public ios_base {
public:
  ...
private:
  basic_ios(const basic_ios&); // not defined
  basic_ios& operator=(const basic_ios&); // not defined
};


C++ 에서는 좀 더 나은 방법이 있다. " = delete" 를 붙여 함수를 "삭제된 함수(deleted function)" 으로 만드는 것이다.

template<class charT, class traits = char_traits<charT>>
class basic_ios : public ios_base {
public:
  ...
  basic_ios(const basic_ios&) = delete;
  basic_ios& operator=(const basic_ios&) = delete;
  ...
};

삭제된 함수는 컴파일 타임에서 에러를 잡아주기 때문에, 기존 C++ 98 방식이 링크 시점에 에러를 발견하는 것에 비해 개선된 형태라고 볼 수 있다. 삭제된 함수는 위처럼 public 으로 선언하는 것이 관례인데, 이는 보통 컴파일러가 접근 지정자(public 인지 private 인지 등)을 먼저 체크하기 때문이다. 즉, 함수를 사용할 수 없는 주된 이유가 접근성 때문이라는 오해의 여지를 제공할 수도 있다.

삭제된 함수의 장점 중 하나는, 그 어떤 함수도 삭제할 수 있다는 것이다. 즉, 원하지 않는 오버로딩 버전의 함수의 경우, 명시적으로 삭제해 버릴 수 있다.

bool isLucky(int number);

// 오버로딩된 함수 버전을 삭제
bool isLucky(char) = delete;
bool isLucky(bool) = delete;
bool isLucky(double) = delete;

if (isLucky(1)) ... // 정상!

if (isLucky('a')) ... // 오류!
if (isLucky(true)) ... // 오류!
if (isLucky(3.5)) ... // 오류!


삭제된 함수로 수행할 수 있는 또 다른 요령은 원치 않는 템플릿 인스턴스화를 방지하는 것이다.

template<typename T>
void processPointer(*ptr);

포인터 세계에는 특별한 포인터가 두 개 있다. 하나는 void* 포인터로, 역참조나 증가, 감소가 아예 불가능하다. 또한 char* 형식의 포인터는 C 스타일 문자열을 가리키는 특별한 포인터다. 이 두 가지 경우가 인자로 전달되는 경우를 processPointer 템플릿이 허용하지 않는다고 하면, 다음과 같이 쓰면 된다.

template<typename T>
void processPointer(void*) = delete;

template<typename T>
void processPointer(char*) = delete;

// 더욱 철저하게 하고 싶다면 const volatile 버전도 삭제
template<typename T>
void processPointer(const volatile void*) = delete;

template<typename T>
void processPointer(const volatile char*) = delete;

// 그 이외의 것들...
template<typename T>
void processPointer(wchar_t*) = delete;

template<typename T>
void processPointer(char16_t*) = delete;

template<typename T>
void processPointer(char32_t*) = delete;


C++98 접근방식으로 오버로딩을 해결하려고 시도하는, 다음과 같은 코드는 컴파일되지 않는다.

class Widget {
public:
  template<typename T>
  void processPointer(void*) {};
  
private:
  // 오류!
  template<>
  void processPointer<void>(void*);
};

이것이 불가능한 이유는, 템플릿 특수화는 반드시 클래스 범위가 아니라 이름공간 범위에서 작성해야 하기 때문이다. 삭제된 함수에는 다른 접근 수준을 지정할 필요가 없으므로 이런 문제가 없다. 아래처럼 멤버 함수를 클래스 바깥(이름공간 범위에서)에서 삭제하는 것은 가능하지만, 그냥 삭제된 함수를 사용하는 것이 더 권장되는 방식이다!

class Widget {
public:
  template<typename T>
  void processPointer(void*) {};
};

template<>
void processPointer<void>(void*) = delete;

 

Comments