로또
9. 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자. 본문
다루는 내용
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 해당 객체의 타입은 바로 기본 클래스이다.
생성자에서 가상 함수를 호출하는 클래스
class Transaction{ // 모든 거래에 대한 기본 클래스
public:
Transaction();
virtual void LogTransaction() const = 0; // 타입에 따라 달라지는 로그 기록을 만든다.
};
Transaction::Transaction(){
// 기본 클래스 생성자 구현
LogTransaction(); // 이 거래를 로깅하기 시작한다.
}
class BuyTransaction: public Transaction{ // Transaction의 파생 클래스
public:
virtual void LogTransaction() const; // 구매 거래 내역 로그를 구현한다.
};
class SellTransaction: public Transaction{
public:
virtual void LogTransaction() const; // 판매 거래 내역 로그를 구현한다.
};
int main(){
BuyTransaction b;
return 0;
}
main()에서 BuyTransaction의 생성자가 호출되기 전에, 기본 클래스가 먼저 생성되어야 하므로 Transaction()의 호출자가 생성된다. 따라서 Transaction 생성자의 LogTransaction() 함수가 호출될 것이다. 호출되는 LogTransaction()함수는 지금 생성되는 클래스가 BuyTransaction임에도 불구하고 BuyTransaction 클래스의 함수가 아니라 Transaction 클래스의 함수이다.
기본 클래스의 생성자가 호출될 동안에는 아직 파생 클래스의 생성자가 호출되기 이전이므로 파생 클래스의 데이터 멤버는 아직 초기화되지 않았을 것이다. 파생 클래스의 가상 함수는 데이터 멤버를 수정할 여지가 충분하므로 초기화되지 않은 변수를 사용하는 것은 미정의 동작을 일으킬 것이다. C++은 이러한 사고를 방지하기 위해 아래처럼 설계되었다.
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 해당 객체의 타입은 바로 기본 클래스이다. 호출되는 가상 함수는 모두 기본 클래스의 것으로 결정되며 dynamic_cast나 typeid같이 런타임 정보를 사용하는 함수에서도 역시 기본 클래스 타입의 객체로 취급한다.
이는 소멸자에서도 마찬가지로, 파생 클래스의 소멸자가 실행된 후에는 파생 클래스의 데이터 멤버는 정의되지 않은 값을 가지기 때문에 C++은 이들을 없는 것으로 취급하고 진행한다. 때문에 파생 클래스의 소멸자가 종료된 후 기본 클래스의 소멸자에 진입할 때에도 가상 함수나 dynamic_cast와 같은 함수들은 모두 기본 클래스 객체의 자격으로 처리한다.
다행히도 위와 같이 기본 클래스의 가상 함수가 순수 가상 함수로 정의된 경우에는 컴파일러에 따라 컴파일 단계에서 오류가 발생할 수도 있다. 대부분 링크 단계에서 Transaction::LogTransaction()의 구현부를 찾지 못해 다음과 같이 오류가 발생할 것이다.

가상함수 대신 비가상함수를 이용하자.
class Transaction{ // 모든 거래에 대한 기본 클래스
public:
explicit Transaction(const std::string& log_info); // 기본 클래스의 생성자에 로그 정보를 요구한다.
void LogTransaction(const std::string& log_info) const; // 비가상함수
};
Transaction::Transaction(const std::string& log_info){
LogTransaction(log_info);
}
class BuyTransaction: public Transaction{ // Transaction의 파생 클래스
public:
BuyTransaction(param...): Transaction(CreateLogString(param...)){
...
}
private:
static std::string CreateLogString(param...);
};
기본 클래스의 LogTranscation() 함수를 비가상함수로 바꾸고, 기본 클래스의 생성자에 로그 정보를 parameter로 요구하도도록 변경하였다. LogTransaction()은 비가상함수이므로 Transaction의 생성자는 LogTransaction()을 안전하게 호출할 수 있다.
기본 클래스 부분이 생성될 떄는 가상 함수를 호출한다 해도 기본 클래스의 울타리를 넘어 내려갈 수 없으므로 필요한 초기화 정보를 파생 클래스 쪽에서 기본 클래스 생성자로 올려주도록 만들어 부족한 부분을 역으로 채워주었다.
또한, CreateLogString() 이라는 정적 함수를 만들었다. 이 함수는 기본 클래스 생성자에 넘길 값을 생성하는 용도로 쓰이는 도우미 함수이다. 정적 멤버로 되어있어 생성이 채 끝나지 않은 BuyTransaction 클래스의 초기화되지 않은 데이터 멤버를 건드릴 위험도 없다.
이것만은 잊지 말자!
- 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 말자. 가상 함수라고 해도, 지금 실행 중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로는 내려가지 않는다.
'책 > Effective C++' 카테고리의 다른 글
| 11. operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자. (0) | 2023.11.08 |
|---|---|
| 10. 대입 연산자는 *this의 참조자를 반환하게 하자. (0) | 2023.11.08 |
| 8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자. (0) | 2023.11.06 |
| 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자. (0) | 2023.11.04 |
| 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자. (0) | 2023.11.03 |