객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하면 안 된다.
가상 함수, 혹은 완전 가상 함수를 사용한다는 것 자체가 다형성 클래스를 사용한다는 것을 의미한다.
이 말은 클래스 간에 상속 관계가 존재한다는 의미인데, 이런 상속 관계의 생성자 안에서의 가상 함수 호출은 무엇을 의미할까?
먼저 아래의 예제를 보자.
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
using namespace std;
class Transaction {
public:
Transaction();
virtual void logTransaction() const = 0; // 순수 가상 함수
};
Transaction::Transaction()
{
cout << "Transaction 생성자 진입\n";
// logTransaction(); // 이 주석을 풀면 컴파일, 실행이 될까?
}
class BuyTransaction : public Transaction {
public:
virtual void logTransaction() const;
};
void BuyTransaction::logTransaction() const {
cout << "BuyTransaction log start...\n";
// // some process...
}
class SellTransaction : public Transaction {
public:
virtual void logTransaction() const;
};
void SellTransaction::logTransaction() const {
cout << "SellTransaction log start...\n";
// some process...
}
int main()
{
BuyTransaction buy;
return 0;
}
일단 위의 코드는 정상적으로 동작한다.
여기서, 주석 처리되어 있는 Transaction 기본 생성자 내부를 주목하자.
완전 가상 함수인 logTransaction 을 호출하려 한다.
그리고 main 문 내부에서는 여전히 BuyTransaction 객체만을 사용한다.
과연 컴파일이 되고 실행이 될까?
결과를 보자면,
컴파일은 잘 되고 링크 에러가 발생하는데, 이 말은 발견하기 훨씬 어렵다는 의미이기도 하다.
어쩌면 위의 에러가 당연하다고 생각할 수 있겠지만, 그 과정을 한 번 생각해보자.
// in main
BuyTransaction buy;
메인문 내부의 해당 코드가 실행되면 당연히 부모 생성자가 먼저 호출되고 자식 생성자가 호출 된다.
( Transaction() → BuyTransaction() )
이 과정 사이에 순수 가상 함수인 logTransaction 멤버 함수를 호출하는 것이다.
( Transaction() → logTransaction() → BuyTransaction() )
BuyTransaction 클래스 내부에는 logTransaction 에 대한 구현이 되어있고,
당연히 부모 클래스에서는 logTransaction 에 대한 구현이 없다. ( 순수 가상 함수 )
자식 객체인 BuyTransaction 을 생성하면, 자신이 재정의한 logTransaction 함수가 호출될까?
→ 아니다, 위에서 본 것처럼 링크 에러가 발생한다.
즉, 아무리 자식 객체를 생성하는 과정에서 호출되었더라도 기본 생성자 내부에서는 가상 함수가 작동하지 않는다는 의미이다.
이것의 근본적인 이유는,
기본 생성자가 동작하는 시점에는 파생 클래스는 데이터 멤버는 아직 초기화 이전의 상태이기 때문이다.
그렇기 때문에 가상 함수 역시 해당 시점에는 동작하지 않는 것이 정상적이다. ( 즉, C++에 존재하는 일종의 안전 장치 )
사실 더 정확하게 말하자면,
기본 생성자가 동작하는 시점에 해당 객체는 무조건 기본 클래스 타입이다.
즉, BuyTransaction buy; 해당 코드에서 부모 생성자가 내부의 시점에서 buy 객체는 Transaction 타입이다.
( dynamic_cast, typeid 등도 모두 해당 순간에는 부모 객체로 인식한다. )
이러한 현상은 소멸자에서도 똑같이 발생한다.
자식 객체 소멸자가 호출되고 끝나는 순간 시점에서 해당 객체는 기본 객체 타입으로 인식되는 수준이 아니라 그냥 기본 객체가 되어버린다.
위의 예제는 이러한 에러를 굉장히 찾기 쉬운편에 속하지만 만약 아래와 같은 경우에는 어떨까?
가상 시나리오
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
using namespace std;
class Transaction {
public:
Transaction()
{
cout << "Transaction 생성자 진입\n";
init();
}
virtual void logTransaction() const // 이제는 순수 가상함수가 아니라 가상 함수이다.그렇기 때문에
{ // Transaction 자체도 logTransaction 구현부를 가지는 것이 어색하지 않다.
cout << "basic logging start...\n";
// some process...
}
private:
void init() { logTransaction(); }
};
class BuyTransaction : public Transaction {
public:
virtual void logTransaction() const {
cout << "BuyTransaction logging start...\n";
// some process...
}
};
//... BuyTransaction 객체처럼
//... 개별적 logTransaction 함수를 재정의한 여러 자식 class...
//... ( 생략 )
class SellTransaction : public Transaction {
public:
// virtual void logTransaction() const;
};
// void SellTransaction::logTransaction() const {
// cout << "SellTransaction logging start...\n";
// // some process...
// }
int main()
{
SellTransaction sell;
cout << "----------------------------\n";
BuyTransaction buy;
return 0;
}
가상 설계 시나리오는 이렇다.
SellTransaction 은 로그 처리에 있어서 별도로 따로 처리할 것이 없어서 기본 클래스에 존재하는 logTransaction 가상 함수를 실행.
BuyTransaction 및 여러 다른 클래스는 자신만의 로그 처리 방식이 필요해서 logTransaction 함수를 재정의. ( 즉, 재정의된 logTransaction 함수의 실행을 기대함 )
만약 위에서 말했던 사실을 모르는 상태로 위와 같이 설계해서 코드를 작성했고 해당 문제점을 프로젝트 초기에 발견하지 못 했다면 상상 이상의 cost가 필요할 수 있다.
에러도 발생하지 않고, 정상적으로 작동하지만 설계 의도와는 다르게 작동하기 때문이다.
해당 문제의 해결법은 굉장히 여러가지가 존재하겠지만, 그 중에서 가상 함수를 비가상 함수로 바꾸는 방법도 존재한다.
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(string parameters) : Transaction(createLogString(parameters)){} //생성자에서 파라미터를 기본생성자로 넘긴다.
private:
static std::string createLogString(string parameters);
};
가상 함수를 활용하는 기존의 방식이 아닌,
자식 객체가 생성되는 시점에 log로 활용될 정보를 비가상 함수인 logTransaction 함수로 넘기고 있다.
즉, logTransaction 함수를 각 객체마다 따로 정의하는 것이 아니라 파라미터를 다른 값으로 전달하는 방식으로 바꾼 것이다.
여기서 또 하나 중요한 점이 createLogString 이라는 정적 멤버 함수이다.
이 함수의 용도는 기본 클래스로 전달하기 위한 파라미터를 제작하는 용도인데,
상속 관계로 인해서 설령 아직 초기화 되지 않은 BuyTransaction 내부의 데이터 멤버를 건드릴 위험도 없기 때문이다.
(Why? 정적 멤버 함수는 정적 멤버 변수만 사용하기 때문이다. )
항목 11) operator= 에서 자기 대입 상황에 대한 처리를 반드시 하자 (0) | 2021.12.05 |
---|---|
항목 10) 대입 연산자는 *this의 참조자 반환을 지향하자 (0) | 2021.12.05 |
항목 8) 소멸자에서의 예외 발생 (0) | 2021.12.05 |
항목 7) 가상 소멸자에 대해서 (0) | 2021.12.05 |
항목 6) 암시적으로 생성되는 함수들을 금지하기 (0) | 2021.12.05 |
댓글 영역