Item 11. 정의되지 않은 private function보다 deleted function을 선호해라

특정 함수를 사용하지 못하게 하는 방법은 무엇이 있을까?

당연한 이야기지만 특정 함수를 구현하지 않으면 사용할 수 없다.

하지만 C++에서는 내가 구현하지 않아도 알아서 만들어지는 함수들이 존재한다.

예를 들어 C++에서 클래스를 구현하면 내가 따로 선언해주지 않아도 컴파일러가 특수 멤버 함수를 만들어 준다.

C++98에서는 특수 멤버 함수로 기본 생성자, 소멸자, 복사 생성자, 복사 대입 연산자가 만들어지고 C++11에서는 이동 생성자, 이동 대입 연산자가 추가되었다.

이처럼 직접 구현하지 않아도 함수가 존재할 수 있으며 내가 원하지 않는 함수를 사용하지 못하게 하는 방법이 필요하다.


정의되지 않은 private function을 이용하는 방법

C++98에서는 특정 함수의 사용을 막기 위해 함수를 private으로 선언하고 함수의 정의를 만들지 않는 방법을 이용했다.

대표적인 예시로 C++ 표준 입출력 라이브러리에 구현된 basic_ios라는 클래스 템플릿이 있다.

입출력 스트림을 나타내는 해당 클래스는 복사 생성 및 대입 연산이 어떠한 행위를 해야 할지 불명확하여 정의되지 않은 private function을 이용하여 사용을 막았다.

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

private으로 생성한 함수는 외부에서 호출할 수 없다. 또한, 멤버 함수나 friend 함수가 private 함수를 호출해도 정의가 없어서 링크에 실패한다.

존재하지만 사용할 수 없는 함수로 만들어 버린 것이다.


deleted function을 이용하는 방법

C++11에서는 함수 선언의 끝에 = delete 키워드를 붙여서 사용을 막을 수 있다.

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;

deleted function은 private이 아닌 public으로 선언하는 것이 관례이다.

그 이유는 deleted function을 외부에서 호출했을 때 C++ 컴파일러가 함수의 접근성을 먼저 확인한 뒤에 삭제 여부를 확인하기 때문이다.

즉, private에 deleted function을 선언하면 호출한 함수가 private이어서 에러가 난 것인지 deleted function이어서 에러가 난 것인지 혼동을 줄 수 있다.


deleted function의 장점

deleted function의 장점은 다음과 같다.

의도가 명확함

deleted function을 사용하면 삭제된 함수를 사용했다는 에러 메시지를 통해 의도를 명확하게 알 수 있다.

에러를 발견하는 시점이 빠름

deleted function을 호출할 경우, 우리는 컴파일 과정에서 에러 메시지를 확인할 수 있다.

하지만 정의되지 않은 private function을 호출할 경우, 링킹 과정에 가서야 에러 메시지를 확인할 수 있다.

즉, deleted function을 사용하면 더 빨리 에러를 발견할 수 있다.

어떤 함수도 삭제할 수 있음

정의되지 않은 private function은 오직 클래스 멤버 함수에서만 사용할 수 있었다. 하지만 deleted function은 어디에서나 사용이 가능하다.

deleted function은 함수 오버로딩에도 유용하게 사용할 수 있다.

// 정수 값을 하나 받고 그 값이 행운의 번호인지의 여부를 돌려주는 비멤버 함수
bool isLucky(int number);

// 인자들이 int가 아니지만 암묵적으로 형변환되어 함수가 호출됨
if (isLucky('a'))
if (isLucky(true))
if (isLucky(3.5f))

// 함수 오버로딩을 통해 타입들을 배제할 수 있음
bool isLucky(char) = delete;
bool isLucky(bool) = delete;
bool isLucky(double) = delete; // double을 삭제하면 float도 배제됨

if (isLucky('a'))  // 에러
if (isLucky(true)) // 에러
if (isLucky(3.5f)) // 에러

원치 않는 템플릿 인스턴스화를 방지함

deleted function은 원치 않는 템플릿 인스턴스화를 방지할 수 있다.

예를 들어 내장 포인터들을 다루는 템플릿을 구현할 때 다른 포인터들과 달리 특별한 처리가 필요한 void*char*를 삭제할 수 있다.

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

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

// 해당 타입을 배제할 때는 const 키워드, 나아가 volatile 키워드까지 삭제해야 함
template <>
void processPointer<const void>(const void*) = delete;
 
template <>
void processPointer<const char>(const char*) = delete;

클래스 안의 함수 템플릿 인스턴스화를 막고 싶으면 정의되지 않은 private function으로는 한계가 있다.

class Widget {
public:
  template <typename T>
  void processPointer(T* ptr) {}
    
private:
  template <>
  void processPointer<void>(void*); //에러
};

같은 이름의 함수 템플릿 중 일부를 못쓰게 하고 싶다고 접근 지정자를 서로 다르게 선언할 수는 없기 때문이다.

이때 deleted function을 사용하면 원하는 함수 템플릿 인스턴스화를 막을 수 있다.

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

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


카테고리:

업데이트: