KoreanFoodie's Study

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

Tutorials/C++ : Advanced

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

GoldGiver 2022. 10. 25. 16:07

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

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

핵심 :

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

 

복사 생성자와 복사 대입 연산자를 통틀어 객체 복사 함수(copying function) 이라고 부른다. 그런데 컴파일러가 이들을 기본으로 만들다 보니, 사용자가 직접 복사 함수를 만들 때는, 구현이 잘못되었을 때도 입을 다물어 버린다. 예시를 보자.

// 고객(customer) 를 나타내는 클래스

// 로그 기록내용을 만든다
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;
}

여기까지는 문제가 없다. 그런데 만약 Customer 클래스에 데이터 멤버 하나를 추가하면 어떻게 될까?

class Date { ... };

class Customer
{
  ...
  private:
    std::string name;
    // 데이터 멤버 추가
    Date lastTransaction;
};

이렇게 되면, 복사 함수의 동작은 완전 복사가 아니라 부분 복사(partial copy)가 된다. 또한 컴파일러는 이런 상황에 대해 경고를 해 주지도 않는다(대부분). 따라서 복사 생성자와 복사 대입 연산자에서 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;
}

위 경우에서는, PriorityCustomer 의 데이터 멤버는 전부 복사하지만, Customer 에서 상속한 데이터 멤버들의 사본은 복사가 되지 않고 있다! PriorityCustomer 의 복사 생성자에는 기본 클래스 생성자에 넘길 인자들도 명시되어 있지 않아서 PriorityCustomer 객체의 Customer 부분은 인자 없이 실행되는 Customer 기본 생성자에 의해 초기화된다. 즉, name 및 lastTransaction 에 대해 '기본적인' 초기화를 해 준다는 뜻이다!

복사 대입 연산자의 경우, 기본 클래스의 데이터 멤버를 건드릴 시도도 하지 않기 때문에, 기본 클래스의 데이터 멤버는 변경되지 않고 그대로 있게 된다.

이를 고치려면, 파생 클래스의 복사 함수 안에서 기본 클래스의 (대응되는) 복사 함수를 호출하도록 만들면 된다.

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

또한 코드 중복을 피하겠답시고 복사 생성자에서 복사 대입 연산자를 호출한다거나, 복사 대입 연산자에서 복사 생성자를 호출하는 경우는 없어야 하겠다. 전자는 논리적으로도 말이 되지 않는 것이, 복사 생성자는 객체를 '초기화' 하는 것이고, 복사 대입 연산자는 '초기화가 완료'된 객체를 대입하는 것이기 때문이다. 마찬가지로, 복사 대입 연산자는 '이미 초기화가 완료된' 객체를 대입 하는 것인데, 복사 생성자를 호출하면 '새로 초기화'를 하는 것이므로 말이 되지 않는다!

코드 중복을 피하려면, 양쪽에서 겹치는 부분을 별도의 함수 (보통 init 어쩌구) 로 만들어 놓고 각 복사 함수에서 호출하는 방식을 사용하도록 하자!

Comments