12: 객체의 모든 부분을 빠짐없이 복사하자
직접 복사 생성자와 복사 대입 연산자 같은 복사 함수를 직접 선언한다면 복사되는 객체가 갖고 있는 데이터를 빠짐없이 복사해야 한다.
void logCall(const std::string& funcName); // 로그 기록내용
class Customer {
public:
Customer(const Customer& rhs)
:name(rhs.name)
{
logCall("Customer copy constructor");
}
Customer& operator=(const Customer& rhs)
{
logCall("Customer copy assignment operator");
name = rhs.name;
return *this;
}
private:
string name;
};
복사 함수를 직접 구현한 Customer클래스이다. 위의 코드는 문제가 없다. 하지만 데이터 멤버 하나를 Customer에 추가한다면 어떻게 될까?
class Customer {
public:
Customer(const Customer& rhs)
:name(rhs.name)
{
logCall("Customer copy constructor");
}
Customer& operator=(const Customer& rhs)
{
logCall("Customer copy assignment operator");
name = rhs.name;
return *this;
}
private:
string name;
Date lastTransaction; // 추가된 데이터
};
Date 객체인 lastTransaction을 추가하게 되면, 복사 함수의 동작은 완전 복사가 아니라 부분 복사가 된다. name은 복사하지만 lastTransaction은 복사하지 않는다. 클래스에 데이터 멤버를 추가했으면, 추가한 데이터 멤버를 처리하도록 복사 함수를 다시 작성할 수 밖에 없다(생성자도 전부 갱신). 비표준형 operator= 함수도 전부 바꿔줘야 한다.
이 문제에 관해 더 심각한 것은 클래스 상속이다.
class PriorityCustomer : public Customer { // 파생 클래스
public:
PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
private:
int priority;
};
PriorityCustomer에 선언된 데이터 멤버를 모두 복사하고 있는 것은 사실이지만 Customer로부터 상속한 데이터 멤버들의 사본도 엄연히 PriorityCustomer에 들어 있는데, 이들은 복사가 안 되고 있다!
PriorityCustomer의 복사 생성자에는 기본 클래스 생성자에 넘길 인자들도 명시되어 있지 않아서, PriorityCustomer 객체의 Customer 부분은 인자 없이 실행되는 Customer의 기본 생성자에 의해 초기화 된다(이 때 기본 생성자가 있다고 가정하자. 없으면 컴파일도 안 된다.) 이 생성자는 당연히 name 및 lastTransaction에 대해 기본적인 초기화를 해준다.
PriortyCustomer의 복사 대입 연산자의 경우에는 기본 클래스의 데이터 멤버는 변경되지 않고 그대로 있게 된다.
파생 클래스에 대한 복사 함수를 직접 만든다면 기본 클래스 부분을 복사에서 빠뜨리지 않도록 해야한다. 기본 클래스 부분은 private 멤버일 가능성이 높기 때문에, 직접 건드리기 어렵다. 그 대신, 파생 클래스의 복사 함수 안에서 기본 클래스의 복사 함수를 호출하도록 만들면 된다.
class PriorityCustomer : public Customer { // 파생 클래스
public:
PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs) // 기본 클래스의 복사 생성자 호출
, priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); // 기본 클래스 부분 대입
priority = rhs.priority;
return *this;
}
private:
int priority;
};
코드 중복을 피하려 복사 대입 연산자에서 복사 생성자를 호출하는 것은 말도 안된다. 이미 만들어진 객체를 생성한다는 것이다. 역으로, 복사 생성자에서 복사 대입 연산자를 호출하는 것도 안된다. 생성자는 새로 만들어진 객체를 초기화하는 역할을 수행하지만, 대입 연산자는 이미 초기화가 완료된 객체에게 값을 주는 것이다.
대신 양쪽에서 겹치는 부분을 별도의 멤버 함수로 분리하여 호출하는 것은 좋은 방법이다. 대개 이런 함수는 private 멤버에 init이라는 이름을 가진다.
이것만은 잊지 말자!
- 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 한다.
- 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 마라. 그 대신, 공통된 동작을 제3의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어 해결하자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자 (1) | 2023.10.10 |
---|---|
[Effective C++] 13: 자원 관리에는 객체가 그만! (0) | 2023.10.09 |
[Effective C++] 11: operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2023.10.07 |
[Effective C++] 10: 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2023.10.06 |
[Effective C++] 09: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (1) | 2023.10.05 |