객체 생성 및 소멸 과정 중에 가상 함수를 호출하면 안되는 이유는 호출 결과가 원하는 대로 돌아가지 않는다.
만약 주식 거래를 본떠 만든 클래스 계통 구조가 있다고 가정해보자.
이 클래스는 매도, 매수, 감사 등등 기능이 있어야 한다. 특히 감사(audit)기능은 중요한 포인트이다.
그렇기 때문에 주식 거래 객체가 생성될 때마다 감사 로그에 적절한 거래 내역이 만들어지도록 해야한다.
/* @brief : 기본 클래스 */
class Transaction
{
public:
Transaction();
virtual void logTransaction() const = 0; //순수 가상 함수
};
Transaction::Transaction()
{
logTransaction(); //생성 중인데 순수 가상함수인 logTransaction 호출
}
/* @brief : Transaction의 파생 클래스 */
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const override;
};
/* @brief : Transaction의 파생 클래스 */
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const override;
};
이 상황에서 BuyTransaction이나 SellTransaction클래스를 생성한다면?
BuyTransaction bt;
SellTransaction st;
파생 클래스인 BuyTransaction이나 SellTransaction보다 기본 클래스인 Transaction의 생성자가 먼저 호출 될 것이다.
Transaction의 생성자에는 순수 가상 함수인 logTransaction를 호출하고 있고, logTransaction은 현재 Transaction의 것이라는 문제가 발생한다.
기본 클래스의 생성자가 호출될 동안, 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않는다.
기본 클래스 생성자가 돌아가고 있을 시점에서는 파생 클래스의 데이터 멤버는 아직 초기화된 상태가 아니라는 것이다.
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은 객체의 타입은 파생 클래스가 아니라 기본 클래스이다.
가상 함수는 모두 기본 클래스의 것으로 결정(resolve)될 뿐 아니라, 런타임 타입 정보를 사용하는 언어 요소(dynamic_cast와 같은)(27장 참고)라던지, typeid를 사용한다고 해도 이 순간엔 모두 기본 클래스 타입으로 취급한다.
애초에 위의 코드는 링크 단계에서 에러가 발생한다.
logTransaction함수가 Transaction클래스 내부에서 순수 가상 함수로 선언되어 있기 때문이다.
정의가 구현되어 있지 않기 때문이다.
만약 Transaction의 생성자가 여러개 된다고 가정해보자.
각 생성자가 하는 일이 조금씩 다르겠지만, 몇 가지 작업은 똑같을 것이다. 이 똑같은 작업을 모아 공동의 초기화 코드로 만들어 두면 코드 판박이 현상을 막을 수 있을까?
class Transaction
{
public:
Transaction()
{
Init();
}
Transaction(size_t data)
{
Init(data);
}
virtual void logTransaction() const = 0; //순수 가상 함수
private:
void Init()
{
a = 1;
logTransaction();
}
void Init(size_t data)
{
a = data;
logTransaction();
}
private:
size_t a;
};
위의 코드는 이전 코드에 비해서 악랄한 코드다. 왜냐하면 앞의 코드와 달리 컴파일도 잘 되고 링크도 말끔하게 되기 때문이다. logTransaction은 Transaction 클래스 안에서 순수 가상 함수 이기 때문에, 대부분의 시스템은 순수 가상 함수가 호출될 때 프로그램을 바로 끝내(abort)버린다.
하지만 logTransaction함수가 만약 보통 가상 함수이고, Transaction 클래스 내부에 구현까지 되어있는 경우엔 골치가 더아파 진다. 순수 가상 함수와 가상 함수의 차이다.(*주 1)
Transaction 버전의 logTransaction이 호출될것이고 아무 문제없이 돌아가다가 파생 클래스 객체가 생성되는데 호출되는 logTransaction 함수는 왜 Transaction의 것인가에 대해 골치가 썩을 것이다.
정리하자면 가상 함수를 쓴다는 것 자체가 파생 클래스에게 정의 혹은 사용을 하라고 물려주는 것인데 파생 클래스에서 구현한 logTransaction이 아니라 기본 클래스인 Transaction의 logTransaction이 실행된다는 것이다.
logTransaction을 Transaction 클래스의 비가상 멤버 함수로 바꾸는 것이다. 그리고 파생 클래스의 생성자들로 하여금 필요한 로그 정보를 Transaction의 생성자로 넘겨야 한다는 규칙을 만든다.
/* @brief : 기본 클래스 */
class Transaction
{
public:
Transaction(const string& logInfo);
void logTransaction(const string& logInfo) const; //일반 멤버 함수
};
Transaction::Transaction(const string& logInfo)
{
logTransaction(logInfo);
}
/* @brief : Transaction의 파생 클래스 */
class BuyTransaction : public Transaction
{
public:
BuyTransaction(string str)
: Transaction(createLogString(str))
{ }
private:
static string CreateLogString(string str);
};
기본 클래스 부분이 생성될 떄는 가상 함수를 호출한다 해도 기본 클래스의 울타리를 넘어 갈 수 없기에, 필요한 초기화 정보를 파생 클래스 쪽에서 기본 생성자로 올려줌으로써 해결할 수 있다.
위의 코드에서 BuyTransaction클래스에 CreateLogString이라는 정적 함수를 사용하고 있다.
이 함수는 기본 클래스 생성자 쪽으로 넘길 값을 생성하는 용도로 쓰고 있는데, 기본 클래스에 멤버 초기화 리스트가 달려 있는 경우 특히 편리하다. 정적 멤버로 되어 있기 때문에, 생성이 끝나지 않은 BuyTransaction 객체의 미초기화된 데이터 멤버를 건드릴 위험도 없다.
미초기화된 데이터 멤버는 정의되지 않은 상태에 있다라는 사실이 있기 때문이다. 기본 클래스 부분의 생성과 소멸이 진행되는 동안 가상 함수가 파생 클래스 쪽으로 내려가지 않는 이유다.
이것만은 잊지 말자
생성자 혹은 소멸자 안에서 가상 함수를 호출하지 말자
가상 함수라고 해도 지금 실행중인 생성자나 소멸자에 해당하는 클래스의 파생클래스 쪽으로 내려가지 않는다.
(*주 1) 순수 가상 함수 vs 가상 함수
순수 가상 함수 : 기본 클래스입장에서 선언만 해두고 정의는 구현하지 않는것
virtual void Move() = 0;
가상 함수 : 기본 클래스에서 선언과 정의 다 해두는 것
virtual void Move() { ... }
이해하기 쉽게 비유해보자면
순수 가상 함수는 "자식아 너 이거 반드시 할 줄 알아야하는 기능인데 니가 알아서 구현해"
가상 함수는 "자식아 너 이거 반드시 할 줄 알아야 하는데 귀찮으면 내껄로 해"
둘 다 파생클래스가 반드시 구현해야하는데 좀 더 친절하게 알려주냐 마냐의 차이라고 생각하면 편함
'서적 정리 > Effective C++' 카테고리의 다른 글
13.자원 관리에는 객체가 그만! (0) | 2021.12.10 |
---|---|
12.객체의 모든 부분을 빠짐없이 복사하자 (0) | 2021.12.10 |
11.operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2021.12.10 |
10.대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2021.12.10 |
8.예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (0) | 2021.12.10 |
7.다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2021.12.10 |
6.컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2021.12.10 |
5.C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (0) | 2021.12.10 |
댓글