로또
6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자. 본문
다루는 내용
5장에서 컴파일러가 은근슬쩍 만들어 호출해 버리는 함수들에 대해 배웠다. 6장에서는 이러한 함수의 생성을 원치 않을 때, 금지해버리는 방법에 대해 다룬다.
객체의 복사본을 생성하고 싶지 않을 때
부동산 중개업 프로그램을 만들 때, "모든 자산은 유일하므로 객체의 사본 생성을 금지하는 경우"를 가정하자.
즉, HomeForSale Class의 복사를 금지하고 싶다.
class HomeForSale{...};
int main(){
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); // 객체 h1을 복사해 객체 h3를 생성한다.
...
return 0;
}
프로그래머가 별도로 복사 생성자나 대입 연산자를 생성하지 않아도 컴파일러는 위와 같은 코드를 보고 복사 생성자를 만들어서 호출하곤 한다.
객체의 복사본을 생성하고 싶지 않다면, 다음과 같은 방법을 시도해볼 수 있다.
1. 복사 생성자 및 대입 연산자를 private 멤버로 선언한다.
컴파일러가 생성하는 함수는 public 멤버로 선언된다.
우리가 복사 생성자 및 대입 연산자를 private하게 직접 선언하면 컴파일러는 마음대로 함수를 만들어 호출할 수 없을 것이며 private 멤버이므로 외부로부터의 호출을 차단할 수 있다.
#include <iostream>
using namespace std;
class HomeForSale{
public:
HomeForSale(){} // 기본 생성자 선언
private:
// 복사 생성자
HomeForSale(const HomeForSale& hfs){
cout << "Copy Constructor" << endl;
}
HomeForSale& operator=(const HomeForSale& rhs){
cout << "Assignment Operator" << endl;
}
};
int main(){
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1);
HomeForSale h4 = h2;
return 0;
}

하지만, private 멤버는 해당 클래스의 멤버 함수 내에서 사용될 수 있으므로 다음과 같이 호출될 수 있다.
#include <iostream>
using namespace std;
class HomeForSale{
public:
HomeForSale(){} // 기본 생성자 선언
void Test(const HomeForSale& hfs){
cout << "Test Start" << endl;
HomeForSale tmp(hfs); // private 복사 생성자 호출
tmp = hfs; // private 대입 연산자 호출
cout << "Test Success" << endl;
}
private:
// 복사 생성자
HomeForSale(const HomeForSale& hfs){
cout << "Copy Constructor" << endl;
}
HomeForSale& operator=(const HomeForSale& rhs){
cout << "Assignment Operator" << endl;
}
};
int main(){
HomeForSale h1;
HomeForSale h2;
h2.Test(h1);
return 0;
}

2. 복사 생성자 및 대입 연선자를 선언만 하고 정의하지 않는다.
위처럼 private멤버로 선언된 복사 생성자와 대입 연산자는 해당 클래스의 멤버 함수나 friend 함수가 호출할 수 있다. 이와 같은 경우를 막기 위해 다음과 같이 함수를 선언만 하고 정의하지 않는다.
#include <iostream>
using namespace std;
class HomeForSale{
public:
HomeForSale(){} // 기본 생성자 선언
void Test(const HomeForSale& hfs){
cout << "Test Start" << endl;
HomeForSale tmp(hfs); // private 복사 생성자 호출
tmp = hfs; // private 대입 연산자 호출
cout << "Test Success" << endl;
}
private:
// 복사 생성자 정의만
HomeForSale(const HomeForSale&);
// 대입 연산자 정의만
HomeForSale& operator=(const HomeForSale&);
};
int main(){
HomeForSale h1;
HomeForSale h2;
h2.Test(h1);
return 0;
}

위 코드를 컴파일하면 사진처럼 링크 시점에서 에러가 발생한다.
복사 방지 클래스 이용하기
링크 시점 에러를 컴파일 시점 에러로 변경할 수 있다.
복사 생성자와 대입 연산자를 private으로 선언하되 이를 HomeForSale 클래스 자체에 넣지 않고 별도의 기본 클래스를 만들어 상속하도록 하는 것이다.
이렇게 생성한 기본 클래스는 복사 방지만을 담당한다.
#include <iostream>
using namespace std;
// 복사 방지 클래스
class UnCopyable{
// 자식에 대해 생성과 소멸 허용
protected:
UnCopyable(){}
~UnCopyable(){}
private:
UnCopyable(const UnCopyable&); // 복사 생성자 선언
UnCopyable& operator=(const UnCopyable&); // 대입 연산자 선언
};
// 복사 방지 클래스를 상속함으로 복사 생성자 및 대입 연산자 호출이 불가하다.
class HomeForSale: private UnCopyable{
public:
HomeForSale(){} // 기본 생성자 선언
void Test(const HomeForSale& hfs){
cout << "Test Start" << endl;
const HomeForSale tmp(hfs);
tmp = hfs;
cout << "Test Success" << endl;
}
};
int main(){
HomeForSale h1;
HomeForSale h2;
h2.Test(h1);
return 0;
}

Boost 라이브러리의 noncopyable 클래스는 위와 같은 복사 방지 클래스로서의 기능을 실제로 적용하고 있다고 한다.
이것만은 잊지 말자!
- 컴파일러에서 자동으로 제공하는 기능을 허용하지 않으려면, 대응되는 멤버 함수를 private으로 선언한 후에 구현은 하지 않은 채로 두자. UnCopyable과 비슷한 기본 클래스를 사용하는 것도 하나의 방법이 될 수 있다.
'책 > Effective C++' 카테고리의 다른 글
| 8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자. (0) | 2023.11.06 |
|---|---|
| 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자. (0) | 2023.11.04 |
| 5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자. (1) | 2023.11.02 |
| 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자. (1) | 2023.11.01 |
| 3. 낌새만 보이면 const를 들이대 보자! (1) | 2023.10.31 |