센로그

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

Effective/Effective C++

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

seeyoun 2024. 11. 17. 02:18

생성자나 소멸자에서 가상함수를 호출하면 안되는 이유

  • 기본 클래스의 생성 과정에서는 가상 함수가 먹히지 않는다.
    • 생성자는 기본 클래스 → 파생 클래스 순으로 호출된다.
    • 이때 유의할 점: 기본 클래스의 생성자가 호출되는 도중에는, 객체의 타입이 기본 클래스가 된다는 것.
      • 기본 클래스의 생성자가 호출되는 동안, 파생 클래스는 초기화된 상태가 아니다.
      • 그런데 가상 함수를 파생 클래스쪽에서 호출하면 파생 클래스의 멤버를 건드릴 가능성이 크다.
      • 따라서 이런 일을 미연에 방지하기 위해, 이 시점에는 객체의 타입을 아예 기본 클래스로 정해버린다.
    • 따라서 호출되는 가상 함수는 모두 기본 클래스의 것으로 결정되며, 런타임 타입 정보(dynamic_cast, typeid 등)도 기본 클래스 타입의 객체로 결정한다.
  • 소멸 과정도 똑같다.
    • 소멸자는 파생 클래스 → 기본 클래스 순으로 호출된다.
    • 파생 클래스의 소멸자가 호출되고 나면, 파생 클래스의 데이터 멤버들은 정의되지 않은 값들이 된다.
    • 따라서 가상 함수가 제대로 동작하기 어려울 가능성이 높다.
    • 또한, 기본 클래스의 소멸자에 진입하면 객체는 기본 클래스 객체가 된다.

 


그렇다면 기본 클래스 초기화 시점에 가상 함수의 기능이 필요할 때는?

  • 기본 클래스 초기화 시점에 가상 함수의 기능이 필요하다는 것은, 생성 시점에 파생 클래스의 타입에 따라 다르게 동작해야할 함수가 필요하다는 것이다. 
    • 그러나 모든 파생 클래스에서 해당 작업을 수행하므로, 기본 클래스에다 통일하고 싶은 것이다.
  • 대처 방법은 많다. 그중 책에서 언급한 하나를 소개한다.
    • 파생 클래스에서 기본 클래스로 정보 넘겨주기

 

기존 예시

// 기본 클래스
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;
    ...
};
  • 모든 객체 생성 시점에 해당 객체의 타입에 따른 logTransaction()을 수행하고 싶다.
  • 이 코드를 통해 BuyTransaction b; 를 선언하고 기본 클래스 생성자가 호출된다면 의도치 않은 동작이 발생한다.
    • 기본 클래스 생성자에서 가상함수 호출 시, 기본 클래스의 함수가 호출되기 때문이다.
      • 내가 원했던 건 타입에 따른 가상함수 호출이었음
    • 물론 위 경우에 logTransaction()은 순수 가상함수이므로 링크 중에 에러 발생함
    • 그러나 순수 가상함수가 아니라면, 그냥 기본 클래스의 것으로 호출될 것임.

 

수정된 예시

class Transaction {
public:
    explicit Transaction(const string& logInfo);
    void logTransaction(const string& logInfo) const;	// 비가상 함수
};

Transaction::Transaction(const string& logInfo) {
    ...
    logTransaction(logInfo);	// 생성자에서 비가상 함수 호출
};

class BuyTransaction : public Transaction {
public:
    // 로그 정보를 기본 클래스 생성자로 넘김
    BuyTransaction(parameters) : Transaction(createLogString(parameters)) { ... };
    ...

private:
    static string createLogString(parameters);
};
  • 기본 클래스 부분이 생성될 때는 가상 함수를 호출한다 해도 기본 클래스의 울타리를 넘어 내려갈 수 없다.
  • 따라서, 기본 클래스 생성자로 필요한 초기화 정보를 받도록 해주고, 이 정보를 파생 클래스 쪽에서 기본 클래스 생성자로 올려주는 것이다.
    • 이는 파생 클래스의 초기화 리스트에서 진행한다.
    • 파생 클래스의 초기화 리스트에서 기본 클래스의 생성자를 호출하면, 해당 정보에 따라 기본 클래스의 생성자가 호출되고 초기화되기 때문이다.

 


생성자/소멸자에 바로 있는 가상함수 뿐만 아니라, 타고 타고 들어가서 호출되는 가상 함수도 있을 수 있다.

  • 따라서 생성자/소멸자에서 호출하는 함수들이 가상 함수를 포함하고 있지는 않은지 잘 살펴보고 수정해야 한다.
Comments