ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체 생성 및 소멸자에 절대로 가상 함수 호출금지
    @ 16. 1 ~ 17. 1/면접관련 2017. 1. 5. 00:09

    C++ 생성자, 소멸자에서는 가상 함수를 호출하면 절대 안됩니다.

    이것은 다른 OOP 언어에서는 지원되는 기능입니다. 하지만 왜 C++에서는 안되는 걸까요?

    이유를 알아보도록 하죠.

    예를 들어서 설명하겠습니다.

    스크롤바 컨트롤를 만들어야 하는데, 스크롤바는 종류가 2개입니다. 수직스크롤바, 수평스크롤바.

    그렇다면, 추상 스크롤바 클래스를 만들고 거기서 상속받은 수직,수평 스크롤바 클래스를 만드는게 좋겠죠?

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

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

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

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

    다른 예로 실제 코드를 보도록 하겠습니다.

    아래는 트랜젝션중 발생하는 이벤트를 로그하는 클래스입니다.

    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
    	...
    };
    

    이렇게 선언한 뒤, 클래스의 인스턴스를 만듭니다.

    BuyTransaction b;

    겉보기엔 문제없이 될것같습니다. 트랜잭션의 로그 함수는 순수 가상 함수 일지라도, 유도 클래스에서 정의를 해놓았으니까요.

    하지만 실제 컴파일을 해보면 링크 에러가 납니다.

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

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

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

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

    위의 트랜젝션 예제에서 좀 더 위험한 문제는 생성함수를 따로 분리해놓는 경우입니다.

    class Transaction {
    public:
    	Transaction()
    	{ init(); }                                      // call to non-virtual...
    
    	virtual void logTransaction() const = 0;
    	...
    
    private:
    	void init()
    	{
    
    		...
    
    			logTransaction();                              // ...that calls a virtual!
    	}
    }; 
    

    생성자에서 init 함수를 통해 간접적으로 순수 가상 함수를 호출하는데, 컴파일러는 생성자에서 순수 가상 함수를 호출하지 않았기 때문에 링크까지 문제없이 됩니다. 하지만 이 경우엔 런타임 에러로 넘어가서 결국 에러가 납니다. 맨 처음 트랜젝션 클래스에서는 생성자 안에서 직접 순수 가상 함수를 호출한 경우였고(컴파일러에 의해 링크 에러로 처리됨), 이번에는 init()메소드를 통해 간접적으로 호출했다는것이 다릅니다.

    생성자에서 가상 함수 호출을 피하는 방법은 로그 함수를 가상 함수로 만들지 않고 다음과 같이 헬퍼 함수를 두는것입니다.

    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 );
    };
    

    중요한 점은 헬퍼함수인 createLogString이 static으로 선언되있다는건데요, 이를 통해 초기화되지 않은 멤버에 접근하는 위험을 막아줍니다.

Designed by Tistory.