HomeForSale 이라는 객체가 있고 이 객체는 세상에 하나밖에 존재하지 않는다고 가정했을때( 모든 자산은 세상에 하나밖에 없죠). HomeForSlae 객체는 사본(copy)을 만드는 것 자체가 이치에 맞지 않습니다. 원래부터 유일한 것을 어떻게 복사할 수 있겠어요? 그러나 보니 HomeForSale 객체를 복사하려 하는 코드는 컴파일이 되지 않았으면 하는 생각이 듭니다.


1
2
3
4
5
6
HomeForSale h1;
HomeForSale h2;
 
HomeForSale h3(h1);            // h1을 복사하려 합니다 - 컴파일 되면 안돼요!!
 
h1 = h2;                    // h2를 복사하려 합니다 - 컴파일하지 말아 주세요!!
cs


주석으로나마 컴파일을 막고 싶은 바람이 어떻게든 정신적 위안은 되겠지만 안 되는 건 이미 아시죠? 세상은 그리 만만하지 않습니다. 일바적인 경우만 놓고 볼 때, 어떤 클래스에서 특정한 종류의 기능을 지원하지 않았으면 하는 의도를 반영하는 방법은 그런 기능을 제공하는 함수를 선언하지 않는 것입니다.


그런데 이 전략은 복사 생성자와 복사 대입 연산자에 대해서는 '해당사항 없음'입니다. 항목 5를 읽은 분은 아시겠지만, 복사 생성자와 복사 대입 연산자는 여러분이 선언하지 않고 외부에서 이들을 호출하려고 하면 컴파일러가 여러분 대신에 이들을 선언해 버리기 때문입니다.


해결의 열쇠는 다음과 같습니다. 바로 컴파일러가 생성하는 함수는 모두 공개된다는, 즉 public 멤버가 된다는 사실입니다. 복사 생성자와 복사 대입 연산자가 저절로 만들어지는 것을 막기 위해 여러분이 직접 선언해야 한다는 점은 맞지만, 이것들을 public 멤버로 선언해야 된다고 요구하는곳은 아무 데도 없다는 점을 기억하셔야 하겠습니다. 그러니까 public멤버로 두지 말고, 복사 생성자 및 복사 대입 연산자를 private 멤버로 선언하도록 합시다. 일단 클래스 멤버 함수가 명시적으로 선언되기 때문에, 컴파일러는 자신의 기본 버전을 만들 수 없게 되지요. 게다가 이 함수들이 비공개(private)의 접근성을 가지므로, 외부로부터의 호출을 차단할 수 있습니다.


여기까지 90점입니다. 10점이 모자라죠, private 멤버 함수는 그 클래스의 멤버 함수 및 프렌드(friend)함수가 호출할 수 있다는 점이 여전히 허점입니다. 이것까지 막으려면, 그러니까 '정의(define)'를 안 해 버리는 기지를 발휘해 보면 어떨까요?


정의되지 않은 함수를 누군가가 어쩌다 실수로 호출하려 했다면 분명히 링크 시점에 에러를 보게 될 테니 괜찮습니다. 실제로 이 꼼수[멤버 함수를 private 멤버로 선언하고 일부러 정의(구현)하지 않는 방법]는 꽤 널리 퍼지면서 하나의 '기법'으로 굳어지기까지 했습니다. C++의 iostream 라이브러리에 속한 몇몇 클래스에서도 복사 방지책으로 쓰이고 있지요. 시간 되시면 여러분이 쓰시는 표준 라이브러리 구현환경에서 ios_base, basic_ios, sentry가 어떻게 만들어져 있는지 확인해 보세요. 복사 생성자와 복사 대입 연산자 모두가 private 멤버로 선언된 동시에 정의되어 있지도 않을 것입니다.


자, 이 꼼수를 HomeForSale에 사용해 봅시다. 어렵지 않습니다.


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



매개변수의 이름이 빠져 있는 게 살짝 거슬릴 수도 있겠습니다만, 선언 시 매개변수 이름은 필수사항이 아닙니다. 그냥 읽기 편하라고 해 주는 관례일 뿐이죠. 어찌 되었든 이들은 앞으로 구현될 예정이 없고, 사용될 일도 없습니다. 그러니 매개변수 이름을 넣을 이유가 어디에 있겠어요?


HomeForSale 클래스는 이렇게 정의되었습니다. 사용자가 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 HomeForSale : private Uncopyable {        // 복사 생성자도,
    ...                                            // 복사 대입 연산자도
}                                                // 이제는 선언되지 않습니다.
cs


원하는 바를 깔끔하게 이루어 주는 코드 입니다. HomeForSale 객체의 복사를 외부(멤버함수나 프렌드 함수까지도)에서 시도하려고 할 때 컴파일러는 HomeForSale 클래스만의 복사 생성자와 복사 대입 연산자를 만들려고 할 것입니다. 항목 12에서 보겠지만, 컴파일러가 생성한 복사 함수는 기본 클래스의 대응 버전을 호출하게 되어 있습니다. 그런데 이런 호출은 지금 통하지 않게 됩니다. 다시다시피 복사 함수들이 기본 클래스에서 공개되어 있지 않기 때문입니다.


마지막으로, Uncopyable의 구현과 사용법에 대해 기술적으로 미묘한 부분 몇 가지를 지적하고 이번항목을 마무리 할까 합니다.


1. Uncopyable로부터의 상속은 public일 필요가 없습니다(항목 32 및 39 참조).

2. Uncopyable의 소멸자는 가상 소멸자가 아니어도 됩니다(항목 7참조).

3. Uncopyable 클래스는 데이터 멤버가 전혀 없기 때문에 항목 39에서 공부하게 될 공백 기본 클래스 최적화(empty base class optimization) 기법이 먹혀 들어갈 여지도 있는데요. 하지만 Uncopyable 클래스는 기본 클래스이기 때문에 이 기법을 사용하면 다중 상속으로 갈 가능성이 있습니다(항목 40 참조). 이번에는 다중 상속이 문제가 되는데, 다중상속 시에는 공백 기본 클래스 최적화가 돌아가지 못할 때가 종종 있습니다(항목 39 참조). 이런 미묘한 부분은 어지간해선 대강 무시하고 살아도 아무 상관이 없습니다. 말씀드린대로 정확히 돌아가니까요.

4. 부스트 라이브러리(항목 55 참조)를 보면 이번 항목의 Uncopyable과 똑같은 구실을 하는 클래스를 찾을 수 있는데, 이것을 써도 됩니다. 이름은 noncopyable로, 무척 괘찮은 클래스입니다. 이름이 아주 조금 비(non)자연, 아니 부(un)자연스럽다는 느낌만 빼면요.



이것만은 잊지 말자!

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







+ Recent posts