1. 가상 함수를 생성자/소멸자에서 호출하는 경우
class Transaction{
public :
Transaction();
virtual void logTransaction() const =0;
// ...
};
Transaction::Transaction(){
// ...
logTransaction();
}
class BuyTransaction: public Transaction{
public :
virtual void logTransaction() const;
// ...
};
// ...
BuyTransaction b;
BuyTransaction의 생성자가 호출되기 전 기본 클래스인 Transaction의 생성자가 먼저 호출되게 된다.
이 때, BuyTransaction이 객체 타입이더라도 Transaction으로 인식된다!
기본 클래스의 생성자가 호출되는 동안에는, 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않는다. 쉽게 말해서 기본 클래스 생성 과정에서는 가상 함수가 먹히지 않는다.
2. 가상 함수가 먹히지 않는 이유
기본 클래스는 파생 클래스보다 앞서 실행되기 때문에, 기본 클래스 생성자가 돌아가고 있을 시점에 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아니다.
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 그 객체의 타입은 기본 클래스이다.(런타임도 포함해서)
다만, 위의 코드는 컴파일러가 사전에 잡아준다. (순수 가상함수 사용)
3. 악랄한 실수
class Transaction{
public :
Transaction(){
init();
}
virtual void logTransaction() const = 0;
...
private :
void init()
{
...
logTransaction();
}
};
위의 코드는 컴파일도 잘 되고 링크도 말끔하게 된다! (디버깅 모드에서는 잡을 수 있는 문제, 릴리즈 모드에서 실행됨)
그러나, 만약 위의 순수 가상 함수가 보통 가상 함수 (순수x)일 때는 디버깅/릴리즈 가리지 않고 아무 문제 없이 돌아간다. 따라서 어디가 잘못 됐는지 파악하기가 어려워진다.
4. 대처방법
가상 함수를 비가상 멤버 함수로 대체하라. 이 경우에 파생 클래스가 이 함수의 내용을 변환하고 싶을 때(가상화의 목적)는 어떻게 할 것인가? 답은 필요한 파라메터를 기본 생성자의 파라메터로 넘겨주는 것이다.
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 객체의 미초기화된 데이터 멤버를 자칫 실수로 건드릴 위험을 없애준다. 이와 같은 이유로 기본 클래스의 생성과 소멸이 진행되는 동안에 호출되는 가상 함수가 무턱대고 파생 클래스로 내려가지 않는 것이다.
이 내용은 항목 4에서 객체 초기화와 함께 다루어져야 한다.
https://sanghun219.tistory.com/139
항목 4에서 말한바와 같이
- 주렁주렁 매달린 초기화 리스트 => 초기화 함수로 처리
- 생성자 내에서 기본 타입 변수가 초기화 되지 않을 수 있다 => 지역 정적 함수의 대입을 통해 초기화
이를 통해 완벽히 이해할 수 있을 것이다.
이것만은 잊지 말자!
- 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요. 가상 함수라고 해도, 지금 실행중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로는 내려가지 않으니까요.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 11. operator =에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2020.07.28 |
---|---|
[Effective C++] 10. 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2020.07.28 |
[Effective C++] 8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (0) | 2020.07.27 |
[Effective C++] 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2020.07.27 |
[Effectivce C++] 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2020.07.27 |