개요
C++에서 객체 지향 프로그래밍의 핵심 기능 중 하나는 가상 함수를 통한 다형성입니다. 그러나 객체의 생성자나 소멸자에서 가상 함수를 호출하는 것은 예상치 못한 동작을 초래할 수 있습니다. 이 글에서는 Effective C++의 9번째 항목을 통해 이 문제의 원인과 해결 방법을 살펴보겠습니다. 이는 특히 복잡한 클래스 계층구조를 다루는 대규모 C++ 프로젝트에서 중요한 개념입니다.
본문
문제 상황
다음과 같은 코드를 고려해봅시다:
class Transaction {
public:
Transaction() {
logTransaction(); // 가상 함수 호출
}
virtual void logTransaction() const {
std::cout << "Base Transaction logged" << std::endl;
}
// ... 다른 멤버들 ...
};
class BuyTransaction : public Transaction {
public:
virtual void logTransaction() const override {
std::cout << "Buy Transaction logged" << std::endl;
}
// ... 다른 멤버들 ...
};
int main() {
BuyTransaction b; // 어떤 로그가 출력될까요?
return 0;
}
C++
복사
여기서 BuyTransaction 객체를 생성할 때, 우리는 "Buy Transaction logged"가 출력되기를 기대할 수 있습니다. 그러나 실제로는 "Base Transaction logged"가 출력됩니다. 왜 그럴까요?
원인 분석
이 현상의 원인은 C++의 객체 생성 순서와 관련이 있습니다:
1.
객체가 생성될 때, 기본 클래스의 생성자가 먼저 호출됩니다.
2.
기본 클래스 생성자 실행 시점에서는 파생 클래스의 멤버 변수들이 아직 초기화되지 않았습니다.
3.
따라서 이 시점에서 가상 함수를 호출하면, 파생 클래스의 버전이 아닌 기본 클래스의 버전이 호출됩니다.
이는 단순히 예상과 다른 동작을 하는 것을 넘어서, 미정의 동작(undefined behavior)으로 이어질 수 있는 심각한 문제입니다.
해결 방법
이 문제를 해결하기 위한 한 가지 방법은 "비가상 인터페이스 관용구"(NVI, Non-Virtual Interface idiom)를 사용하는 것입니다:
class Transaction {
public:
Transaction(const std::string& logInfo) : logInfo_(logInfo) {
logTransaction(logInfo_); // 비가상 함수 호출
}
void logTransaction(const std::string& logInfo) const {
// 공통 로깅 로직
doLogTransaction(logInfo); // 가상 함수 호출
}
protected:
virtual void doLogTransaction(const std::string& logInfo) const {
std::cout << "Base: " << logInfo << std::endl;
}
private:
std::string logInfo_;
};
class BuyTransaction : public Transaction {
public:
BuyTransaction(const std::string& logInfo)
: Transaction(createLogString(logInfo)) {}
protected:
virtual void doLogTransaction(const std::string& logInfo) const override {
std::cout << "Buy: " << logInfo << std::endl;
}
private:
static std::string createLogString(const std::string& logInfo) {
return "Buy - " + logInfo;
}
};
C++
복사
이 방식에서는:
1.
기본 클래스의 생성자에서 비가상 함수를 호출합니다.
2.
이 비가상 함수는 내부적으로 가상 함수를 호출할 수 있습니다.
3.
파생 클래스는 정적 함수를 통해 필요한 정보를 기본 클래스 생성자에 전달합니다.
이렇게 하면 객체 생성 과정에서의 예상치 못한 동작을 방지하면서도, 다형성의 이점을 활용할 수 있습니다.
개인적 견해
C++을 학습하면서, 이러한 세부적인 언어 동작을 이해하는 것이 얼마나 중요한지 깨달았습니다. 특히 객체 지향 프로그래밍의 핵심인 다형성을 다룰 때, 이런 세부사항을 간과하면 예기치 못한 버그에 직면할 수 있습니다.
비록 제가 C++로 대규모 프로젝트를 진행한 경험은 없지만, 이 원칙은 모든 객체 지향 언어에 적용될 수 있는 중요한 개념이라고 생각합니다. 다른 언어로 프로젝트를 진행할 때도 객체의 초기화 순서와 메서드 호출 시점을 신중히 고려해야 했습니다.
결론
객체의 생성 및 소멸 과정에서 가상 함수 호출을 피하는 것은 안전하고 예측 가능한 C++ 프로그램을 작성하는 데 필수적입니다. 이 원칙을 준수함으로써 개발자는 복잡한 클래스 계층 구조에서도 객체의 일관된 초기화와 안전한 사용을 보장할 수 있습니다.