본문 바로가기

C++/Effective C++

[C++] 9. 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자

class Transaction {
public:
    Transaction();
    virtual void logTransaction() const = 0;
    ...
};

Transaction::Transaction()
{
    ...
    logTransaction();
}

class BuyTransaction: public Transaction {
public:
    virtual void logTransaction() const;
    ...
};

class SellTransaction: public Transaction {
public:
    virtual void logTransaction() const;
    ...
};

BuyTransaction b;

 

해당 코드를 보면 BuyTransaction의 생성자에서 logTransaction()은 BuyTransaction의 함수일 것 같지만, 실제로는 Transaction에서 호출된다. 즉, 기본 클래스의 생성자가 호출되는 동안에는 가상 함수가 절대로 파생 클래스 쪽으로 내려가지 않는다. 그 대신 객체 자신이 기본 클래스 타입인 것처럼 동작한다.

지금 해드린 이야기보다 사실 다섯 배는 더 핵심적인 이야기를 꺼낼 때가 왔습니다. 파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 그 객체의 타입은 바로 기본 클래스 입니다. 호출되는 가상 함수는 모두 기본 클래스의 것으로 결정(resolve)될 뿐만 아니라, 런타임 타입 정보를 사용하는 언어 요소[이를테면 dynamic_cast(항목 27 참조)라든지 typeid같은것]를 사용한다고 해도 이 순간엔 모두 기본 클래스 타입의 객체로 취급합니다.

 

즉, BuyTransaction 객체의 기본 클래스 부분을 초기화하기 위해 Transaction 생성자가 실행되는 동안에는 그 객체의 타입이 Transaction이라는 것이다.  

 

그러나 애초에 해당 함수가 순수 가상 함수로 선언되어 있으므로, 함수를 정의하지 않으면 링크 단계에서 오류가 발생한다. 결과적으로 그런 코드가 오히려 낫다고 볼 수도 있다.

class Transaction {
public:
    Transaction()
    { init(); }

    virtual void logTransaction() const = 0;
    ...

private:
    void init()
    {
        ...
        logTransaction();
    }
};

 

해당 코드가 의미하는 것은, 일반 가상 함수나 멤버 버전이 구현되어 있을 경우 컴파일도 링크도 정상적으로 이루어지기 때문에 오류를 감지할 방법이 없다는 것이다.  

이러한 문제를 해결하기 위한 방법은 여러 가지가 있지만, 한 가지를 예로 들면 logTransaction을 Transaction 클래스의 비가상 멤버 함수로 변경하는 것이다.  


그리고 파생 클래스의 생성자들이 필요한 로그 정보를 Transaction의 생성자로 전달하도록 규칙을 정한다면, logTransaction이 비가상 함수이기 때문에 Transaction 생성자는 이 함수를 안전하게 호출할 수 있다.

class Transaction{
public:
    explicit Transaction(const std::string& logInfo);
    void logTransaction(const std::string& logInfo) const;
    ...
};

Transaction::Transaction(const std::string& logInfo)
{
    ...
    logTransaction(logInfo);
}

class BuyTransaction: public Transaction{
public:
    BuyTransaction(parameters): Transaction(createLogString(parameters)){ ... }
    ...
private:
    static std::string createLogString(parameters);
};

 

위 코드를 설명하자면, 기본 클래스 부분이 생성되는 동안에는 가상 함수를 호출하더라도 파생 클래스 쪽으로 내려갈 수 없다.  
따라서 필요한 초기화 정보를 파생 클래스에서 기본 클래스의 생성자로 ‘올려’ 주도록 설계한 것이다.