09: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자
주식 거래를 본떠 만든 클래스 계통 구조가 있다고 가정하자.
class Transaction
{
public:
Transaction();
virtual void logTransaction() const = 0;
};
Transaction::Transaction()
{
logTransaction(); // 기본 클래스 생성자 구현
}
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
BuyTransaction b;
BuyTransaction 객체 b가 생성될 때 어떻게 될까? 파생 클래스 객체가 생성될 때 그 객체의 기본 클래스 부분이 파생 클래스 부분보다 먼저 호출된다. 일단 Transaction 생성자가 호출된다. 여기서 logTransaction이 호출되는데 이 함수는 BuyTransaction의 것이 아니라 Transaction의 것이다. 기본 클래스의 생성자가 호출될 동안에는, 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않는다.
기본 클래스 생성자가 돌아가고 있을 시점에 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아니다. 이때 기본 클래스 생성자에서 어쩌다 호출된 가상 함수가 파생 클래스 쪽으로 내려간다면 어떻게 될까? 파생 클래스 버전의 가상 함수는 파생 클래스만의 데이터 멤버를 건드릴 것이 뻔하다. 하지만 그 데이터는 아직 초기화되지 않았기에 미정의 동작이 발생한다. 그러므로 C++는 이런 실수조차 하지 못하도록 막은 것이다.
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 그 객체의 타입은 바로 기본 클래스이다. 이 순간에 모두 기본 클래스 타입의 객체로 취급된다. BuyTransaction 클래스만의 데이터는 아직 초기화된 상태가 아니기에, 아예 없었던 것처럼 취급하는 편이 최고로 안전하다는 것이다.
객체의 소멸에서도 마찬가지다. 파생 클래스의 소멸자가 일단 호출되고 나면 파생 클래스만의 데이터 멤버는 정의되지 않은 값으로 가정하기 때문에, 이제부터 C++는 이들은 없는 것처럼 취급하고 진행한다.
위의 코드에서 logTransaction은 가상 함수이다. 이번 항목을 위반한 것이고, 여기서는 쉽게 발견된다. 또한 logTransaction은 순수 가상 함수인데 링크 단계에서도 에러가 발생한다. 구현부가 없기 때문이다.
만약 생성자의 동일한 부분을 함수로 묶어서 처리한 다음 코드는 어떨까?
class Transaction
{
public:
Transaction();
virtual void logTransaction() const = 0;
private:
void init()
{
logTransaction();
}
};
Transaction::Transaction()
{
init();
}
앞의 코드와 달리 컴파일도 잘 되고 링크도 잘 된다. 이렇게 되면 머리가 아파진다.
이 문제의 대처법은 logTransaction을 Transaction 클래스의 비가상 멤버 함수로 바꾸는 것이다. 그러고 나서 파생 클래스의 생성자들로 하여금 필요한 로그 정보를 Transaction의 생성자로 넘겨야 한다는 규칙을 만든다.
class Transaction
{
public:
explicit Transaction(const string& logInfo);
virtual 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);
};
필요한 초기화 정보를 파생 클래스 쪽에서 기본 클래스 생성자로 올려주도록 만듦으로써 부족한 부분을 역으로 채울 수 있다. 위의 createLogString이라는 정적 함수가 사용되고 있다. 기본 클래스 생성자 쪽으로 넘길 값을 생성하는 용도로 사용하며 멤버 초기화 리스트가 너무 길 경우 특히 편리하다. 정적 멤버로 되어 있기 때문에, 생성이 채 끝나지 않은 BuyTransaction 객체의 미초기화된 데이터 멤버를 자칫 실수로 건드릴 위험도 없다.
이것만은 잊지 말자!
- 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 말자. 가상 함수라고 해도, 지금 실행중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로는 내려가지 않는다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 11: operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2023.10.07 |
---|---|
[Effective C++] 10: 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2023.10.06 |
[Effective C++] 08: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (1) | 2023.10.05 |
[Effective C++] 07: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2023.10.03 |
[Effective C++] 06: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2023.10.02 |