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

Jangmanbo·2023년 3월 20일
0

Effective C++

목록 보기
9/33

다음은 주식 거래(매도 주문, 매수 주문, ...)를 본떠 만든 클래스들이다.

// 모든 거래에 대한 기본 클래스 (순수 가상 클래스)
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;

TransAction 생성자가 호출된 뒤 BuyTransaction 생성자가 호출된다.
그런데 위의 코드를 보면, TransAction 생성자 본문 마지막에 logTransaction을 호출한다.
아마 사용자가 의도한 것은 BuyTransactionlogTransAction이겠지만, 실제로는 TransactionlogTransaction가 호출될 것이다. 객체는 기본 클래스 생성 과정에서는 기본 클래스 타입인 것처럼 동작하기 때문이다. (그리고 logTransaction의 정의가 존재하지 않으므로 링크 에러가 발생한다.)

파생 클래스 객체의 기본 클래스 부분이 생성되는 동안, 객체 타입은 기본 클래스!

  • 호출되는 가상함수는 모두 기본 클래스의 가상함수로 결정
  • 런타임 타입 정보를 사용하는 언어 요소(dynamic_cast, typeid, ...)를 사용하더라도 모두 기본 클래스 객체로 취급

즉, BuyTransaction 객체를 생성하기 위해 기본 클래스인 Transaction의 생성자가 실행하는 동안은 객체 타입이 Transaction이라는 것이다.

따라서 기본 클래스의 생성자가 호출되는 동안, 가상 함수는 절대 파생 클래스 쪽으로 내려가지 않는다.


왜 그런지는 가상함수가 파생 클래스 쪽으로 내려간 경우를 가정해보면 알 수 있다.
  1. 기본 클래스 생성자가 동작하는 시점에 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아님
  2. 이때 기본 클래스 생성자에서 호출된 가상 함수가 파생 클래스 쪽으로 내려감
  3. 파생 클래스 버전 가상 함수는 아직 초기화되지 않은 파생 클래스만의 데이터 멤버에 접근

이러한 일이 발생하게 되므로 C++은 사용자가 이런 실수조차 하지 못하도록 막은 것이다.


소멸자

참고로 객체가 소멸될 때도 마찬가지이다.
파생 클래스의 소멸자가 호출되면 파생 클래스만의 데이터 멤버는 정의되지 않은 값으로 가정한다.
따라서 C++은 파생 클래스만의 데이터 멤버들은 없는 것처럼 취급하고 진행한다.

파생 클래스 객체가 기본 클래스 소멸자에 진입할 당시, 객체 타입은 기본 클래스!

  • 가상함수, dynamic_cast 등 C++ 기능 모두 기본 클래스 객체의 자격으로 처리

하지만 생성자/소멸자 안에서 가상 함수가 호출되는지 확인하는 것은 쉽지 않다.
아마 대부분의 사용자들은 Transaction의 생성자가 여러 개이고 각 생성자에서 하는 작업 중 공통 작업들은 하나의 초기화 코드로 만들어 둘 것이다.
만약 이러한 private 비가상 초기화 함수 내에서 가상 함수인 logTransaction을 호출한다면??

class Transaction {
public:
	Transaction() { init(); }	// 비가상 멤버 함수 호출
    
    virtual void logTransaction() const = 0;
    
    ...
private:
	void init() { logTransaction(); }	// 비가상 함수에서 가상 함수 호출
};

처음에 설명한 코드와 개념적으로는 동일하지만, 이전 코드와 달리 위의 코드는 컴파일에 이어 링크도 성공하여 프로그램이 정상적으로 작동할 것처럼 보인다.

그러나 이 코드는 더 큰 문제를 낳는다.

  1. logTransaction이 순수 가상함수
    -> 순수 가상함수를 호출한 순간 바로 프로그램이 종료(abort)
  2. logTransaction이 일반 가상함수
    -> TransactionlogTransaction을 호출. 즉, Transaction의 멤버만 초기화되어 이후 프로그램 작동에 문제 발생

해결법

  1. logTransactionTransaction 클래스의 비가상 멤버 함수로 변경
  2. 파생 클래스의 생성자들이 필요한 로그 정보를 Transaction의 생성자로 넘김
  3. logTransaction이 비가상 함수이므로 Transacton의 생성자가 이 함수를 안전하게 호출
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)) { ... }	// 로그 정보를 Transaction 생성자로 넘김
    
private:
	static std::string createLogString(parameters);
    ..
};

createLogString

  • 기본 클래스 생성자에게 넘길 값을 생성하는 용도로 쓰이는 도우미 함수
  • 기본 클래스의 멤버 초기화 리스트가 많을 때 편리
  • static이므로 생성이 끝나지 않은 BuyTransaction 객체의 미초기화 데이터 멤버를 건드릴 위험도 없음

정리
객체의 생성 및 소멸 중에는 객체를 기본 클래스 타입으로 취급
-> 파생 클래스의 가상함수가 아닌 기본 클래스의 가상함수를 호출
-> 생성자/소멸자에서 가상 함수를 호출하지 말자!

0개의 댓글