이전에 언급했다시피 복사 생성자, 복사 대입 연산자는 기본적으로 컴파일러가 생성해준다.
컴파일러가 만들어주는 코드는 지극히 심플한 목적을 수행하는데, 해당 객체의 모든 데이터를 직접 복사 ( Deep Copy ) 하는 것.
그리고 당연한 말이지만, 해당 함수들을 프로그래머가 직접 정의하는 순간부터 컴파일러는 이러한 자동 생성 행위를 하지 않는다.
문제는 복사 되지 않는 데이터에 대한 경고조차 해주지 않는다는 점이다.
아래의 예시를 보자.
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; // 항목 10
}
위와 같이 복사 생성자와 복사 대입 연산자를 직접 구현하고, 객체 내부 데이터(name)를 빠짐없이 복사한다면 아무런 문제가 되지 않는다.
그런데 위의 코드가 작성된 이후에 누군가 아래와 같은 코드를 추가한다면 어떻게 될까?
class Data{...};
class Customer{
public:
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
private:
std::string name;
Data lastTransaction; // 추가된 데이터
};
//... 나머지 코드 생략
즉, 특정 객체의 복사 관련 함수가 이미 구현되어있는 상황에서, 내부 데이터 멤버만 따로 추가되는 상황인 것이다.
당연하겠지만 위의 코드에서 lastTransaction 데이터는 복사 처리가 제대로 이루어지지 않는다.
즉, 복사 생성자나 복사 대입 연산자를 통해서 완전 복사가 아니라 부분 복사가 일어난다는 것이다.
이러한 상황에 대해서 컴파일러가 경고를 해줄까?
→ No, 경고해줄 근거가 전혀 존재하지 않는다. 컴파일러 입장에서는 부분 복사가 의도된 것인지, 실수인지 판단할 수 없기 때문이다.
void logCall(const std::string& funcName) {
static int logNum = 0;
std::cout << funcName << " 로그 작성... " << logNum++ << "\n";
} // 로그 기록내용 만듦
class Customer {
public:
Customer(std::string pname, int pid);
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
void PrintData();
private:
std::string name;
int id;
};
Customer::Customer(std::string pname, int pid): name(pname), id(pid) {}
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; // 항목 10
}
void Customer::PrintData() {
std::cout << name << "\n";
std::cout << id << "\n";
}
int main()
{
Customer c1("name1", 112);
Customer c2(c1);
c2.PrintData();
return 0;
}
위와 같이 부분 복사가 일어난 상황에 대해서 컴파일러는 어떠한 경고도 해주지 않는다.
즉, 이미 복사 생성자 및 복사 대입 연산자가 직접 정의되어있는 객체에 대해서
특정 멤버 데이터를 추가하려면 복사 관련 함수를 모두 같이 검토해야 한다는 의미가 되는 것이다. ( 다중 생성자 역시 마찬가지 )
그 밖에도 상속 관계의 객체 역시 해당 상황을 함께 검토 해야 하는데, 부모 객체와 자식 객체 관계라도 private 형식의 데이터에 대한 처리는 직접 해야 하기 때문이다.
사실 이러한 상황은 아래의 코드처럼 파생 클래스에서 기본 클래스의 복사 생성자 및 복사 대입 연산자를 호출하는 형식을 습관화 한다면, 미연에 방지가 가능하다.
또한 자기 자신이 가진 데이터를 객체 자신이 복사 처리하는 것이 가장 자연스럽기 때문에 이해하기도 더 쉬워진다.
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
:priority(rhs.priority), Customer(rhs) // 부모 복사 생성자 호출 ( 초기화 리스트 )
{
logCall("PriorityCustomer copy construcor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
Customer::operator=(rhs); // 부모 복사 대입 연산자 호출
return *this;
}
항목 14) 자원 관리 클래스의 복사 동작에 대해서 (0) | 2021.12.05 |
---|---|
항목 13) 객체를 활용한 자원 관리 ( RAII 패턴 ) (0) | 2021.12.05 |
항목 11) operator= 에서 자기 대입 상황에 대한 처리를 반드시 하자 (0) | 2021.12.05 |
항목 10) 대입 연산자는 *this의 참조자 반환을 지향하자 (0) | 2021.12.05 |
항목 9) 객체 생성 및 소멸 중에 가상 함수 호출 (0) | 2021.12.05 |
댓글 영역