728x90
반응형

C++98에서는 어떤 함수의 사용을 막고 싶으면, private으로 선언하고 함수의 정의를 만들지 않는 방법을 사용하였다.

// C++98
template <class charT, class traits = char_traits<charT>>
class basic_ios : public ios_base
{
public:
...
private:
    basic_ios(const basic_ios& ); // 정의 안함
    basic_ios& operator=(const basic_ios&); // 정의 안함
};

이렇게 private로 선언하면 클라이언트가 함수를 호출할 수 없다. 즉, 멤버 함수나 friend클래스가 정의하지 않은 함수를 사용하면 링크는 함수 정의 누락으로 인해 실패한다.

 

C++11에서는 더 좋은 방법이 있는데, 사용을 막을 함수 선언의 끝에 "=delete"를 붙이면 된다. 이러한 함수를 삭제된 함수(deleted function)라고 부른다.

사용을 막아둔 함수를 사용하려고 했을 경우, C++98은 링크 시점에 가서야 발견되지만, C++11의 삭제된 함수는 컴파일 시점에 발견할 수 있다.

여기서 삭제된 함수는 private이 아니라 public으로 선언하는 것이 관례인데 여기에는 이유가 있다.

클라이언트 코드가 멤버 함수를 사용하려고 할 때, C++은 먼저 그 함수의 접근성을 점검한 후에야 삭제 여부를 점검한다. 그런데 private 함수를 사용하는 클라이언트 코드에 대해 그 함수가 private이라는 점을 문제 삼는 컴파일러들이 있다. 함수를 사용할 수 없는 주된 이유가 함수의 접근성 때문이 아니기 때문에 이는 정확한 오류 메시지의 출력에 방해가 될 수 있으며, 따라서 삭제된 함수는 private로 선언하는 것보다 public으로 선언했을 때 대체로 더 나은 오류 메시지가 나온다.

// C++11 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;
};

삭제된 함수의 중요한 장점은 비멤버 함수와 템플릿 인스턴스를 비롯한 그 어떤 함수도 삭제할 수 있다는 점이다.

파라미터로 정수 값을 하나 받는 비멤버 함수가 있다고 하자. 이때, 수치로 간주될 여지가 조금이라도 있는 타입은 암묵적으로 int로 변환되는데 그래서 아래의 예시처럼 논리적이지도 않은 코드들이 무사히 컴파일된다.

bool isLucky(int number);

// 논리적이지 않음
if (isLucky('a'))
...

if (isLucky(true))
...

if (isLucky(3.5))
...

파라미터가 반드시 정수이어야 한다면, 위의 호출들이 아예 컴파일되지 않게 만드는 것이 바람직하다.

따라서 배제할 타입들에 대해 오버로딩된 함수들을 명시적으로 삭제하는 것으로 이 문제를 해결할 수 있다.

bool isLucky(int number); // 원래의 함수
 
bool isLucky(char) = delete; // char을 배제
bool isLucky(bool) = delete; // bool을 배제
bool isLucky(double) = delete; // double과 float를 배제
 
// 주석에서 보듯이, double을 받는 오버로딩된 함수를 삭제하면 double과 float이 모두 배제된다.
// 이는 float가 int와 double 모두로 변환될 수 있는 상황에서는 double로 변환되기 때문이다.

if (isLucky('a')) // 오류! 삭제된 함수를 호출하려 함
if (isLucky(true)) // 오류!
if (isLucky(3.5f)) // 오류!

 

또 다른 활용 방법으로는 원치 않는 템플릿 인스턴스화를 방지하는 것이다.

아래의 예시를 봐보자.

// 예를 들어 내장 포인터들을 다루는 다음과 같은 템플릿이 있다고 하자.
template <typename T>
void processPointer(T* ptr);
 
// 이 중에서 void* 타입이나 char*타입의 포인터로는 processPointer를 호출하지 못하게 하고 싶다고 하자.
// 이것은 그냥 해당 템플릿 인스턴스들을 삭제하면 된다.
 
template <>
void processPointer<void>(void*) = delete;

template <>
void processPointer<char>(char*) = delete;
 
// 이제 void*나 char*로 processPointer를 호출하는 것은 유효하지 않다.
// 일반적으로, void*나 char*가 안 된다면 const void*나 const char*도 안 되어야 마땅하기 때문에 해당 인스턴스들도 삭제할 필요가 있다.
 
template <>
void processPointer<const void>(const void*) = delete;

template <>
void processPointer<const char>(const char*) = delete;
 
// 더욱 철저하게 하고 싶다면 const volatile void* 오버로딩 버전과 const volatile char* 버전, 그리고 다른 표준 문자 타입들인 wchar_t, char16_t, char32_t의 포인터들에 대한 버전들도 삭제해야 할 것이다.
// 여기서 클래스 안의 함수 템플릿의 일부 인스턴스화를 방지하려는 목적으로 private 선언 접근방식은 적용할 수 없다.
// 멤버 함수 템플릿의 한 특수화의 접근 수준을 그 템플릿 자체의 것과는 다른 수준으로 지정하는 것은 불가능하기 때문이다.
// 예를 들어 processPointer가 Widget 클래스의 한 멤버 함수 템플릿이고, void* 포인터로 그 템플릿을 호출하지 못하게 한다고 할 때, 다음과 같은 C++98 접근방식은 컴파일되지 않는다.
 
class Widget
{
public:
    ...
    template <typename T>
    void processPointer(T* ptr)
    { ... }
    
private:
    template <> // 오류!
    void processPointer<void>(void*);
};
 
// 이것이 불가능한 이유는, 템플릿 특수화는 반드시 클래스 범위가 아니라 이름공간 범위에서 작성해야 한다는 것이다.
// 하지만 삭제된 함수에는 다른 접근 수준을 지정할 필요가 없으므로 이런 문제가 없어 멤버 함수를 클래스 바깥에서 삭제하는 것이 가능하다.
 
class Widget
{
public:
    ...
    template <typename T>
    void processPointer(T* ptr);
    { ... }
    ...
};

// 여전히 public이지만 삭제되었다.
template <>
void Widget::processPointer<void>(void*) = delete;

 

요약

  • 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하자
  • 삭제된 함수는 비멤버 함수와 템플릿 인스턴스를 비롯한 그 어떤 함수도 삭제할 수 있다.
728x90
반응형