본문 바로가기
Book & Lecture/Effective C++

Effective C++ 항목12 : 객체의 모든 부분을 빠짐없이 복사하자

by studio ODOC 2023. 2. 19.
반응형

Effective C++ 항목12 : 객체의 모든 부분을 빠짐없이 복사하자

 

선요약

  1. 객체 복사 함수는 주어진 객체의 모든 데이터를 복사해야한다.
    1. 모든 데이터 멤버
    2. 상속 받았다면 기본 클래스 부분
  2. 클래스의 복사 함수를 두 개 구현할 때, 한 쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대금지. 대신에 공통된 동작을 제3의 함수에 분리해놓고 양 쪽에서 이를 호출하게 만들어서 해결한다.

 

복사생성자와 복사 대입 연산자를 통틀어 객체 복사 함수라고 부른다.

개발자가 직접 복사 함수를 만들 때에는 클래스가 완전히 복사되지 않더라도 컴파일러가 에러나 경고를 표시하지 않는다.

 

void logCall(const std::string& funcName);

class Customer
{
  public:
    ...
    Customer(const Customer& rhs);
    Customer& operator=(const Customer& rhs);
    ...
    
  private:
    std::string name;
    Date lastTransaction; // 기본적으로 복사되지 않는 데이터 멤버
};

// 복사 생성자
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;
}

 

위 복사 함수들은 name 변수에 대해서는 복사가 되지만, lastTransaction 변수에 대해서는 복사하지 않는다.

이 경우에 컴파일러는 경고조차 하지 않는다!

 

따라서 이 복사 함수들에 lastTransaction를 생성 및 대입하는 코드가 필요하다.

 

파생 클래스의 경우에는 개발자를 더욱 괴롭히는 문제가 생긴다.

// 파생 클래스
class PriorityCustomer : public Customer
{
  public:
    ...
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& rhs);
    ...
    
  private:
    int priority;
};

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;
}

부모 클래스인 Customer의 데이터 멤버에 대해 복사가 되지 않고 있다. 파생 클래스 생성자에서 Customer 부분은 Customer의 생성자(기본 생성자)에 의해 초기화 된다. 하지만 파생 클래스 복사 대입 연산자의 경우에는 사정이 다르다. 기본 클래스의 데이터 멤버를 건드릴 시도조차 하지 않기 때문에 기본 클래스 데이터 멤버에 대해 아무런 복사가 이뤄지지 않는다.

 

이를 해결하기 위해, 기본 클래스의 데이터 멤버를 직접 복사해도 되지만 보통 private선언되어 있기 때문에 기본 클래스의 복사 함수를 호출하도록 하자.

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs),
  priority(rhs.priority) // 해결책) 기본 클래스의 복사 생성자 호출
{
  logCall("PriorityCustomer copy Constructor");
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
  logCall("PriorityCustomer copy assignment operator");
  
  Customer::operator=(rhs); // 해결책) 기본 클래스의 복사 대입 함수 호출
  priority = rhs.priority;
  return *this;
}

이 때, 코드 중복을 피하겠다는 이유로 복사 생성자에서 복사 대입 연산자를 호출하는 식의 호출은 하지 않아야 한다.

복사 생성자는 '초기화'이고, 복사 대입 연산자는 '초기화가 완료된 객체를 대입'하는 것이기 때문이다. 그래서 서로가 서로를 호출하는 것이 말이 되지 않는다.

그 대신에 공통된 동작을 제3의 함수에 분리해놓고 양 쪽에서 이를 호출하게 만들어서 해결한다.

반응형