본문 바로가기

업무용 언어/C++

[Effective C++] 항목 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자



[Effective C++] 항목 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자

객체 복사 막기

부동산과 관련된 다음과 같은 클래스가 있다고 가정해보자.

class HomeForSale { ... };

모든 자산은 세상에 하나 밖에 없기 때문에 위 클래스로 만들어지는 객체는 복사가 불가능하게 해야한다.

HomeForSale home1;
HomeForSale home2;

HomeForSale home3(home1); // home1을 복사하려 한다.

home1 = home2; // home2를 복사하려 한다.

일반적으로 어떠한 기능을 막고 싶다면, 그런 기능을 지원하는 함수를 선언하지 않으면 된다. 그러나 복사 생성자와 복사 대입 연산자는 컴파일러가 자동으로 만들어 내기 때문에(항목 5 참조) 이런 방법이 통하지 않는다.

그렇다면 어떻게 해야 할까? 바로 컴파일러가 자동 생성하는 함수는 public이 되기 때문에 private으로 미리 만들어 버리면 된다. 일단 이들을 만들어 두기만 한다면, 컴파일러는 절대로 자동 생성을 하지 않는다.

여기까지만 하면 될까? 아니다. 약간 부족하다. private 멤버 함수는 그 클래스의 멤버 함수와 friend 함수가 호출할 수 있다는 사실이 떡하니 버티고 있다. 이것까지 막으려면 어떻게 하는 것이 좋을까? 바로 ‘정의’를 일부로 하지 않으면 된다. 어쩌다 실수로 복사 생성자나 복사 대입 연산자를 호출했다면, 링크 시점에 ‘정의되지 않은 함수’라는 에러를 보게 될테니 말이다. 이 꼼수는 하나의 기법으로 굳어져 표준 라이브러리인 iostreamios_base, basic_ios등에서도 쓰이고 있다고 한다.

자, 그럼 복사가 불가능한 버전의 HomeForSale을 보자.

class HomeForSale {
public:
    ...
private:
    HomeForSale(const HomeForSale&); // 선언만 달랑 있다.
    HomeForSale& operator=(const HomeForSale&); // 마찬가지
};

매개 변수 이름이 빠져 있지만, 이는 필수 사항이 아니며, 매개 변수 이름을 선언한다해도 사용할 일이 없으니 뺐다.

이제 사용자가 HomeForSale 객체를 복사하려 한다면 컴파일 시점 에러를 보게 될것이고, 여러분이 깜빡하고 HomeForSale의 멤버 함수 혹은 friend 함수 안에서 객체를 복사하려 한다면 링크 시점 에러를 보게 될 것이다.

링크 시점 에러를 컴파일 시점 에러로 바꾸기

추가로 링크 시점 에러를 컴파일 시점 에러로 옮길 수도 있다. 에러는 나중에 발견되는 것보다 미리 발견되는 것이 더 좋기 때문에 이 방법을 추천한다. 바로 복사 불가 속성을 가지는 기반 클래스로부터 상속을 받는 방법이다.

class Uncopyable {
protected: // 파생된 클래스에 대해
    Uncopyable() { } // 생성과
    ~Uncopyable() { } // 소멸을 허용
private:
    Uncopyable(const Uncopyable&); // 하지만 복사는 방지
    Uncopyable& operator=(const Uncopyable&);
};

이제 HomeForSale을 다음과 같이 바꿔주자.

class HomeForSale: private Uncopyable {
    ...
};

HomeForSale 객체의 복사를 시도할 때, 컴파일러는 복사 생성자 혹은 복사 대입 연산자를 생성하려고 할 것이다. 그러나 컴파일러가 자동 생성한 복사 생성자나 복사 대입 연산자는 기반 클래스의 복사 생성자나 복사 대입 연산자를 먼저 호출해야 하지만, 해당 함수들이 private으로 선언되어 있기 때문에 생성이 되지 않으며, 컴파일 에러가 발생한다.

여기서 기술적으로 미묘한 몇가지 사항을 짚고 넘어가자면, 우선 Uncopyable로부터의 상속은 public 상속일 필요가 없다(항목 32 및 39 참조). 그리고 Uncopyable의 소멸자는 가상 소멸자가 아니어도 된다(항목 7 참조). 또한 Uncopyable 클래스는 데이터 멤버가 전혀 없기 때문에 공백 기반클래스 최적화(EBO: Empty Base Optimization)(항목 39 참조) 기법을 쓸 여지가 생기는데, Uncopyable은 기반 클래스이기 때문에 이 기법을 사용하면 다중 상속(항목 40 참조)으로 갈 가능성이 있다. 그러나, 다중 상속시에는 EBO 기법이 돌아가지 못할 때가 종종 있으니 이런 미묘한 부분은 대강 무시하고 넘어가자. 또한 부스트(boost) 라이브러리에는 Uncopyable와 똑같은 구실을 하는 noncopyable이라는 클래스가 있는데, 이 클래스를 사용해도 된다.

  • 컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를 private으로 선언한 후에 구현은 하지 않은 채로 두자. Uncopyable과 비슷한 기반 클래스를 상속해서 사용하는 것도 한 방법이다.