http://www.informit.com/articles/article.asp?p=397656&rl=1링크: Effective C++: Never call virtual functions during construction or destruction

얼마전에 스크롤바 컨트롤를 만들고 있었는데, 스크롤바는 종류가 2개입니다. 수직스크롤바, 수평스크롤바.

기본적으로 추상 스크롤바 클래스를 만들고 거기서 상속받은 수직,수평 스크롤바 클래스를 만드는게 좋겠죠?

대부분의 기능은 추상클래스에 구현해놓고 수직,수평스크롤바의 차이점만 유도클래스에서 지정할수 있도록 순수가상함수를 만들었습니다.

스크롤바 생성자는 Rect클래스를 받는데 Rect클래스 멤버인 left, top, right, bottom 중 수직스크롤바는 top, bottom을, 수평스크롤바는 left, right멤버에 엑서스 하는 순수가상함수를 만든거죠.

생성자에선 이 가상함수를 호출해서 스크롤바에 달린 상하,좌우스크롤버튼이나 Thumb버튼을 또 만들어야 합니다.

그런데 여기서 문제가 발생했습니다. 스크롤바 생성자에서 선언되고 수직, 수평 스크롤바에서 구현된 가상 함수를 호출하려니 링크 에러가 납니다.


같은 예제이니 여기부터는 사이트 링크(ec++ 3rd)에 걸린 예를 들겠습니다. 영어로 보실분은 링크 보세요.

아래는 트랜젝션중 발생하는 이벤트를 로그하는 클래스입니다.
[code]
class Transaction { // base class for all
public: // transactions
Transaction();

virtual void logTransaction() const = 0; // make type-dependent
// log entry
...
};
Transaction::Transaction() // implementation of
{ // base class ctor
...
logTransaction(); // as final action, log this
} // transaction
class BuyTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
// actions of this type
...
};
class SellTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
// actions of this type
...
};
[/code]
이러고 인스턴스를 만듭니다.
[code]
BuyTransaction b;
[/code]
겉보기엔 문제없이 될것같습니다. 트랜잭션의 로그함수는 순수가상함수인데, 유도클래스에서 정의를 해놓았으니까요.

하지만 링크에러로 안됩니다.

위와같이 base클래스와 derived 클래스가 있을때, derived 클래스 인스턴스를 만들면 derived 생성자가 호출되기 이전에 base 생성자가 호출됩니다. 이때 아직 derived생성자가 호출되지 않았으므로 derived 멤버변수는 초기화되지 않은 상태입니다. 만약 base 생성자에서 순수 가상함수를 호출했는데 그 순수가상함수가 derived 클래스의 멤버에 접근한다고 하면, 이것은 분명 에러입니다.

따라서 base클래스의 생성자에서는 - 그 오브젝트가 실제로 derived 인지 base인지 상관없이 - base 객체로 취급됩니다. 이렇게 해야 derived 멤버에 엑서스 하는것을 막을 수 있고 순수가상함수역시 호출되지 않죠.

이 증거를 들어보면, 위의 예제에서 트랜잭션의 로그함수를 순수가상함수가 아닌 일반가상함수로 만들고 컴파일해보세요. 링크, 실행까지 문제없이 되지만 바이트랜젝션의 로그함수가 호출되는게 아니라 트랜잭션의 로그함수가 호출됩니다. 물론 객체를 트랜잭션클래스로 취급해버렸기 때문에 일어난일이며 의도하지않은 결과입니다.

소멸자에서도 마찬가지입니다. base 클래스와 그의 유도클래스인 derived 클래스의 인스턴스가 있을때, 객체가 소멸되면 derived의 소멸자가 먼저 호출되죠. 그리고 나중에 base의 소멸자가 호출됩니다. 만약 base소멸자에서 순수가상함수를 엑서스 한다면, 이미 소멸되고 없는 derived의 멤버에 엑서스할 위험이 있습니다. 따라서 역시 안되죠.

위의 트랜젝션 예제에서 좀더 위험한 문제는 생성함수를 따로 분리해놓는 경우입니다.
[code]
class Transaction {
public:
Transaction()
{ init(); } // call to non-virtual...

virtual void logTransaction() const = 0;
...

private:
void init()

{

...

logTransaction(); // ...that calls a virtual!
}
};
[/code]
생성자에서 init 함수를 통해 간접적으로 순수가상함수를 호출하는데, 컴파일러는 생성자에서 순수가상함수를 호출하지 않았기때문에 링크까지 문제없이 됩니다. 하지만 이 경우엔 런타임에러로 넘어가서 결국 에러가 납니다.

생성자에서 가상함수 호출을 피하는 방법은 로그 함수를 가상함수로 만들지 않고 다음과 같이 헬퍼 함수를 두는것입니다.
[code]
class Transaction {
public:
explicit Transaction(const std::string& logInfo);

void logTransaction(const std::string& logInfo) const; // now a non-
// virtual func
...
};

Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); // now a non-
} // virtual call

class BuyTransaction: public Transaction {
public:
BuyTransaction( parameters )
: Transaction(createLogString(

parameters

)) // pass log info
{ ... } // to base class
... // constructor

private:
static std::string createLogString( parameters );
};
[/code]
중요한점은 헬퍼함수인 createLogString이 static으로 선언되있다는건데요, 이를 통해 초기화되지 않은 멤버에 접근하는 위험을 막아줍니다.

blog: http://ljh131.tistory.com
email: ljh131@gmail.com