- 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 한다.
- 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하지 마라. 그 대신, 공통된 동작을 제3의 함수에다 분리해 놓고 양쪽에서 호출해라.
객체를 복사할 때 주의할 점
객체의 안쪽 부분을 캡슐화한 객체 지향 시스템 중 설계가 잘 된 것들을 보면, 복사 함수가 복사 생성자와 복사 대입 연산자 딱 둘만 있다. 이러한 복사 함수는 컴파일러가 필요에 따라 만들어내기도 한다. 컴파일러가 만든 복사함수는 기본적인 요구에 충실히 동작한다. 복사되는 객체가 갖고 있는 데이터를 빠짐없이 복사한다.
객체 복사 함수를 선언하는 것은 컴파일러가 생성하는 것보다 추가적인 동작을 하기 위해서일 것이다.
void logCall(const std::string& funcName); // 로그 기록내용 생성
class Customer {
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs): name(rhs.name) // 복사 생성자
{
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs) // 복사 대입 연산자
{
logCall("Customer copy assignment operator");
name = rhs.name;
return *this;
}
고객을 나타내는 클래스가 있다고 가정해 보자. 문제가 없어 보이지만 다음과 같이 데이터 멤버 하나를 추가하면 문제가 생길 수 있다.
class Date { ... };
class Customer{
public:
...
private:
std::string name;
Date lastTransaction;
};
복사 함수는 객체의 모든 멤버를 복사하지 못하고 부분 복사를 한다. name은 복사하지만 lastTransaction은 복사하지 않는다. 하지만 컴파일러는 이를 허용하고 넘어간다. 이를 해결하려면 직접 클래스 데이터를 추가하고 복사 함수에도 업데이트해야 된다는 뜻이다.
클래스를 상속할 때도 주의해야 한다.
class PriorityCustomer: public Customer{
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priotity;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
PriorityCustomer의 모든 멤버를 복사하는 것처럼 보이지만, Customer로 부터 상속한 데이터 멤버들은 복사되지 않는다.
PriorityCustomer의 복사 생성자에는 기본 클래스 생성자에 넘길 인자들도 명시되어 있지 않아서 PriorityCustomer 객체의 Customer 부분은 인자없이 실행되는 Customer의 기본 생성자에 의해 초기화된다.
PriorityCustomer의 복사 대입 연산자의 경우에는 기본 클래스의 데이터 멤버는 변경되지 않고 그대로 있게 된다.
복사 대입 연산자는 기본 클래스의 데이터 멤버를 건드리지 않기 때문이다.
파생 클래스에 대한 복사 함수를 만드는 상황에는 기본 클래스 부분을 복사에서 빠뜨리지 않도록 주의해야 한다.
물론 기본 클래스 부분은 private 멤버일 가능성이 높기 때문에 직접 건드리기는 어려울 수 있다.
그 대신, 파생 클래스의 복사 함수 안에서 기본 클래스의 복사 함수를 호출하도록 만들면 된다.
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), priority(priority) // 기본 클래스의 복사 생성자 호출
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); // 기본 클래스 부분 대입
priority = rhs.priority;
return *this;
}
정리하자면 객체를 복사할 때는 두 가지를 주의해야 한다.
- 해당 클래스의 데이터 멤버를 모두 복사해라.
- 클래스가 상속한 기본 클래스의 복사 함수를 호출해라.
사실, 복사 생성자와 복사 대입 연산자의 내용이 비슷한 경우가 자주 있다. 그래서 하나를 구현하고 다른 한쪽에서 호출하게 만들려는 생각을 할 수 있지만 그렇게 해서는 안 된다.
복사 대입 연산자에서 복사 생성자를 호출하는 것부터 말이 안 된다. 이미 만들어진 객체를 생성하는 것이기 때문이다.
그리고 복사 생성자에서 복사 대입 연산자를 호출하는 것도 말이 안된다. 생성자는 새로 만들어진 객체를 초기화하는 것이지만, 대입 연산자는 이미 초기화가 끝난 객체에게 값을 주는 것이다. 즉, 생성 중인 객체에 대입을 하는 꼴이다.
하지만, 코드가 겹치는 것을 피하기 위해서 겹치는 부분을 별도의 멤버 함수에 분리해 놓은 후 이 함수를 호출하게 만들 수 있다. 보통 이 함수를 private 멤버로 지정한 후 init과 같은 이름을 가지게 한다.