본문 바로가기
Working on

[Effective C++] 객체의 모든 부분을 빠짐없이 복사하자

by Warehaus 2021. 5. 20.

객체지향 시스템에서 잘 설계된 것들을 보면 객체를 복사하는 함수가 딱 둘만 있는 것을 알 수 있다.

복사 생성자와 복사대입 연산자가 그 두가지 함수인데, 이 둘을 통틀어서 객체 복사 함수 (copying function) 이라고 불러진다.

객체 복사함수 선언의 의미는 컴파일러 동작을 100% 신뢰할 수 없다는 것을 의미하는데, 아래 코드를 보자.

void logCall( const std::strign& funcName); // 로그 기록내용 생성

class Apple {
public :
    ....
    Apple ( const Apple& a );
    Apple& operator=(const Apple& a );
    ...
private:
    string brix;
};

Apple::Apple( const Apple& a ) : brix( a.brix ) // a 의 데이터 복사
{
    logCall ( *Apple copy constructor* );
}


Apple& Apple::operator=(const Apple& a )
{
    logCall(*Apple copy assignment operator*);
    name = a.brix; // a의 데이터 복사
    return *this; // 대입연산자 operator는 *this 를 반환, C++ 세계에서의 de-Facto
}

위 코드는 그닥 문제가 없는 코드이다. 그런데 멤버변수가 추가되는 경우, 생각처럼 동작하지 않게된다.

class Apple {
public :
    ....
    Apple ( const Apple& a );
    Apple& operator=(const Apple& a );
    ...
private:
    string brix;
    string producer;
};

사과의 생산자 정보가 추가되었는데, 이런경우 복사함수의 동작이 완전복사가 아닌 부분복사(Partial copy)가 된다.

brix 값은 operator= 에 구현되어 있어 복사되지만, producer는 복사되지 않기 때문이다. 

여기서 문제는 이렇게 복사 값이 누락 될 가능성이 있음에도, 컴파일러가 알려주지 않는다는게 문제라는 것이다.

이 문제는 클래스 상속이 일어나는 경우에도 발생하는데, 아래 코드를 보자

class GreenApple : public Apple {

public:
    ...
    GreenApple ( const GreenApple& a );
    GreenApple& operator= ( const GreenApple& a );
    ...
private:
    int color;
};

GreenApple::GreenApple( const GreenApple& a ) : color( a.color )
{
    logCall( "Copy constructor for color." );
}

GreenApple& GreenApple::operator= (const GreenApple& a)
{
    logCall("GreenApple copy assignment operator");
    
    color = a.color;
    
    return *this;
}

GreenApple 클래스의 복사함수는 GreenApple의 모든 내용을 복사하는 것으로 보인다. 그런데 Apple클래스의 데이터들도 상속을 받았 기 때문에 GreenApple 이 갖게 되는데, 이 부분은 복사가 되지 않고 있다.

GreenApple 복사 생성자에는 기본 클래스 생성자에 넘길 인자들도 명시가 되지 않아 Apple의 기본 생성자에 의해서만 초기화 된다. 이에따라 brix와 producer에 대한 값만 초기화를 해주게 될 것이다.

결론적으로 상속을 하게되는 경우 하위 클래스에서 상위 클래스의 데이터 복사에 대해 주의가 필요한데, 이는 하위 클래스의 복사함수에서 상위 클래스의 복사함수를 만들어 처리가 가능하다.

GreenApple::GreenApple( const GreenApple& a) : Apple(a), color( a.color)
{
    logCall( "GreenApple copy constructor" );
}

GreenApple& GreenApple::operator= ( const GreenApple& a )
{
    logCall( " GreenApple copy assignment operator");
    
    Apple::operator=(a); // 기본 클래스 부분을 대입한다.
    color = a.color;
    
    return *this;
}

복사 함수를 만들 때, 모든 부분을 복사가 필요하다는 말이 이런 상속이나 새로 멤버변수가 추가되는 경우 이에 상응하는 추가 구현이 필요하다는 것을 의미하며 이를 위해서 아래 두 가지를 확인하면 될 것이다.

1. 해당 클래스의 데이터 멤버를 모두 복사

2. 상속 시 상위클래스 복사함수의 호출

복사 생성자와 복사 대입 연산자에 나타나는 코드 중복을 제거하기 위해서는 중복 코드를 별도의 멤버함수에 분리 후 해당 함수를 호출하게 하는 것이다.  이런 함수는 대체로 private 멤버로 두고, init_*** 이름을 갖게 된다.