KoreanFoodie's Study

Effective C++ | 항목 9 : 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 본문

Tutorials/C++ : Advanced

Effective C++ | 항목 9 : 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자

GoldGiver 2022. 10. 25. 16:05

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

항목 9 : 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자

핵심 :

생성자 및 소멸자 안에서 가상 함수를 호출하지 말자! 가상 함수라고 해도, 지금 실행 중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로는 내려가지 않는다!

 

객체 생성 및 소멸 과정 중에는 가상 함수를 호출하면 절대로 안 된다! 예시를 보자.

// 모든 거래에 대한 기본 클래스
class Transaction
{
  public:
    Transaction();
    // 타입에 따라 달라지는 로그 기록을 만든다
    virtual void logTransaction() const = 0;
    ...
};

// 기본 클래스 생성자의 구현
Transaction::Transaction()
{ 
  // 해당 거래를 로깅한다
  logTransaction(); 
}

// Transaction 의 파생 클래스
class BuyTransaction : public Transaction
{
  public:
    // 이 타입에 따른 거래내역 로깅을 구현한다
    virtual void logTransaction() const;
    ...
}

...

// Q. 이 코드가 실행될 때 무슨 일이 일어날까?
BuyTransaction b;

BuyTransaction b; 코드가 실행될 때, 생성자가 호출이 되는데, 먼저 기반 클래스의 생성자가 호출될 것이다. 그런데 기반 클래스의 생성자는 순수 가상 함수이다. 문제는, 기본 클래스의 생성자가 호출될 때는 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않기 때문에, 우리가 원하는 동작이 제대로 수행되지 않는다는 것이다.

수정 : 그런데 사실 위의 경우처럼, 생성자에서 "순수 가상 함수" 를 호출하는 경우, 아예 컴파일 자체가 안된다. 더 적절한 예시는 다음과 같을 것이다.
// 모든 거래에 대한 기본 클래스
class Transaction
{
  public:
    Transaction();
    // 타입에 따라 달라지는 로그 기록을 만든다
    virtual void logTransaction() const { std::cout << "Base" << std::endl; }
};

// 기본 클래스 생성자의 구현
Transaction::Transaction()
{ 
  // 해당 거래를 로깅한다
  logTransaction(); 
}

// Transaction 의 파생 클래스
class BuyTransaction : public Transaction
{
  public:
    // 이 타입에 따른 거래내역 로깅을 구현한다
    virtual void logTransaction() const { std::cout << "Buy" << std::endl; }
};

// Q. 이 코드가 실행될 때 무슨 일이 일어날까?
int main() {
    // 출력 : Base
    BuyTransaction b;
    return 0;
}

위의 예시에서, 우리가 의도한 출력은 "Buy" 일 것이나, 실제로는 "Base" 가 출력된다.

이는 기반 클래스에서 가상 함수를 호출할 때, 파생 클래스는 아직 초기화가 되지 않았기 때문에, 해당 가상 함수는 파생 클래스의 가상함수를 찾아 내려가지 않기 때문이다. 만약 파생 클래스가 초기화 되지 않은 상태에서 파생 클래스의 가상함수를 찾으려고 하면, 실제로 찾을 수 없기 때문에 '미정의 동작 무한열차' 를 타게 된다. 따라서 생성자에서의 가상 함수는 현재 호출되는 객체의 가상 함수를 호출하게 되는 것이다.

 

이 문제는 다음과 같이 간단하게 해결할 수 있다. 먼저, logTransaction 을 Transaction 클래스의 비가상 멤버 함수로 바꾸는 것이다. 그러고 나서 파생 클래스의 생성자들로 하여금 필요한 로그 정보를 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 );
};

즉, 기본 클래스 부분이 생성될 때는 가상 함수를 호출해도 기본 클래스의 울타리를 벗어날 수 없기 때문에, 필요한 초기화 정보를 파생 클래스 쪽에서 기본 클래스 생성자로 '올려' 보냄으로써 부족한 부분을 역으로 채울 수 있다.

createLogString 함수는 정적 멤버로 선언되어 있기 때문에, 생성이 채 끝나지 않은 BuyTransaction 객체의 미초기화된 데이터 멤버를 자칫 실수로 건드릴 위험도 없다. 왜냐하면 '미초기화된 데이터 멤버는 정의되지 않은 상태에 있다' 라는 사실 때문인데, 정적 멤버인 createLogString 함수는 정적으로 선언되지 않은 데이터 멤버를 건드리지 않을 것이기 때문이다!! (static 함수는 non-static 한 멤버 변수를 사용할 수 없다)

Comments