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

생성자, 소멸자 및 대입 연산자, 두 번째 이야기

Posted by SungBeom on November 24, 2019 · 6 mins read

컴파일러가 만들어낸 복사 생성자, 복사 대입 연산자

경우에 따라 특정 객체가 사본(copy)를 만드는 것 자체가 이치에 맞지 않는 경우가 있습니다. 유일해야만 하는 객체들이 그렇습니다. 예를 들어 매물로 내놓은 가옥을 나타내는 클래스가 있다고 칩시다.
class HomeForSale { ... };
일반적인 경우만 놓고 볼 때, 어떤 클래스에서 특정한 종류의 기능을 지원하지 않았으면 하는 의도를 반영하는 방법은 그런 기능을 제공하는 함수를 선언하지 않는 것입니다. 하지만 이 전략은 복사 생성자와 복사 대입 연산자에 대해서는 불가능합니다. 복사 생성자와 복사 대입 연산자는 사용자가 선언하지 않고 외부에서 이들을 호출하려고 하면 컴파일러가 대신 이들을 선언해 버리기 때문입니다.

1
2
3
4
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); // 복사 생성자
h1 = h2;            // 복사 대입 연산자
cs

컴파일러의 복사를 막는 방법

해결의 열쇠는 컴파일러가 생성하는 함수는 모두 공개된다는, 즉 public 멤버가 된다는 사실입니다. 복사 생성자와 복사 대입 연산자가 저절로 만들어지는 것을 막기 위해 사용자가 직접 선언하되, 이를 private 멤버로 선언하도록 합시다. 일단 클래스 멤버 함수가 명시적으로 선언되기 때문에, 컴파일러는 자신의 기본 버전을 만들 수 없게 되지요. 이 함수들이 비공개(private)의 접근성을 가지므로, 외부로부터의 호출을 차단할 수 있습니다.

private 멤버 함수는 그 클래스의 멤버 함수 및 프렌드(friend) 함수가 호출할 수 있다는 점이 여전히 허점입니다. 이것까지 막으려면, 그러니까 '정의(define)'를 안 해 버리는 기지를 발휘해 보면 어떨까요? 정의되지 않은 함수를 누군가가 어쩌다 실수로 호출하려 했다면 분명히 링크 시점에 에러를 보게 될 테니 괜찮습니다. 이 기법은 C++의 iostream 라이브러리에 속한 몇몇 클래스에서도 복사 방지책으로 쓰이고 있습니다. ios_base, basic_ios, sentry는 복사 생성자와 복사 대입 연산자 모두가 private 멤버로 선언된 동시에 정의되어 있지도 않을 것입니다.

1
2
3
4
5
6
7
8
class HomeForSale {
public:
    ...
 
private:
    ...
    HomeForSale(const HomeForSale&);  // 선언만 달랑 있습니다.
    HomeForSale& operator=(const HomeForSale&);
cs

사용자가 HomeForSale 객체의 복사를 시도하려고 하면 컴파일 에러가 발생할 것이고, 여러분이 깜박하고 멤버 함수 혹은 프렌드 함수 안에서 그렇게 하면 링크 에러가 발생할 것입니다. 링크 시점 에러를 컴파일 시점 에러로 옮길 수도 있습니다(에러 탐지는 나중으로 미루는 것보다 미리 하는 것이 좋습니다). 복사 생성자와 복사 대입 연산자를 private로 선언하되, 이것을 HomeForSale 자체에 넣지 말고 별도의 기본 클래스에 넣고 이것으로부터 HomeForSale을 파생시키는 것입니다. 그리고 그 별도의 기본 클래스는 복사 방지만 맡는다는 특별한 의미를 부여합니다.

1
2
3
4
5
6
7
8
9
class Uncopyable {
protected:                          // 파생된 객체에 대해서
    Uncopyable() {}                 // 생성과 소멸을
    ~Uncopyable() {}                // 허용합니다.
 
private:
    UnCopyable(const Uncopyable&);  // 하지만 복사는 방지합니다.
    Uncopyable& operator=(const Uncopyable&);
};
cs

복사를 막고 싶은 HomeForSale 객체는 이제 이렇게 바꿔 봅시다. Uncopyable로부터 상속받게 하고 그냥 내버려 두는 것으로 끝입니다.

1
2
3
class HomeForSlae: private Uncopyable {  // 복사 생성자도,
...                                      // 복사 대입 연산자도
};                                       // 이제는 선언되지 않습니다.
cs

컴파일러가 생성한 복사 함수는 기본 클래스의 대응 버전을 호출하게 되어 있습니다. 그런데 이런 호출은 지금 통하지 않게 됩니다. 복사 함수들이 기본 클래스에서 공개되어 있지 않기 때문입니다.

Uncopytable의 구현과 사용법에 대해 기술적으로 몇 가지 짚고 넘어갈 점은, Uncopyable로부터의 상속은 public일 필요가 없습니다. 그리고 Uncopyable의 소멸자는 가상 소멸자가 아니어도 됩니다. 또한 Uncopyable 클래스는 데이터 멤버가 전혀 없기 때문에 공백 기본 클래스 최적화(empty base class optimization) 기법이 먹혀 들어갈 여지도 있습니다. 하지만 Uncopyable 클래스는 기본 클래스이기 때문에 이 기법을 사용하면 다중 상속으로 갈 가능성이 있습니다. 다중 상속 시에는 공백 기본 클래스 최적화가 돌아가지 못할 때가 종종 있습니다. 부스트 라이브러리를 보면 Uncopyable과 똑같은 구실을 하는 noncopyable이란 무척 괜찮은 클래스를 찾을 수 있는데, 이것을 써도 됩니다.


정리

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