개발/C, C++

[C++] 생성자에서 virtual 가상 함수 호출하는 경우의 문제

growing-dev 2023. 8. 11. 21:51
반응형

오늘은 생성자에서 가상함수를 호출하는 경우에 무엇이 문제가 되는지 알아보도록 하겠습니다.

 

 

생성자에서 virtual 가상 함수 호출하는 경우의 문제

 

 

 

 이론적인 내용

 

C++ 생성자에서 가상 함수를 호출하는 것은 조심해야 하는 상황입니다. 가상 함수 호출은 객체의 실제 타입에 따라 동적으로 호출되는데, 생성자에서 가상 함수를 호출하면 예상치 못한 동작이 발생할 수 있습니다.

C++에서 객체의 생성 과정은 다음과 같습니다:

- 메모리 할당
- 기본 클래스의 생성자 호출
- 파생 클래스의 생성자 호출

가상 함수 호출은 객체의 실제 타입에 따라 동적으로 결정되는데, 생성자의 경우 파생 클래스의 생성자가 호출되기 전에 기본 클래스의 생성자가 호출되므로 파생 클래스의 가상 함수는 아직 초기화되지 않은 상태일 수 있습니다.

가상 함수 호출을 생성자에서 사용하는 것은 다음과 같은 문제를 일으킬 수 있습니다:

- 파생 클래스의 가상 함수가 제대로 초기화되지 않았을 수 있음.
- 파생 클래스에서 생성자에서 필요한 초기화 작업이 완료되지 않은 상태에서 가상 함수가 호출될 수 있음.
- 가상 함수가 순수 가상 함수인 경우 (pure virtual function) 해당 함수를 호출할 수 없어 프로그램이 정상적으로 동작하지 않을 수 있음.


따라서 생성자에서 가상 함수를 호출하는 것은 피하는 것이 좋습니다. 생성자 내에서 가상 함수 호출이 필요한 경우에는 다른 설계 방식을 고려하거나 초기화 리스트를 사용하여 생성자 호출 순서를 조정해야 합니다.

 

 

 예제 코드 소개

 

예제 코드를 통해 조금 더 자세히 이해해 보도록 하겠습니다.

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent() {
        cout << "Parent construct" << endl;
    }
    ~Parent() {
        cout << "Parent destruct" << endl;
    }
};

class Child : public Parent
{
public:
    Child() {
        cout << "Child construct" << endl;
    }
    ~Child() {
        cout << "Child destruct" << endl;
    }
};

int main()
{
    auto* child = new Child;
    delete child;
}

 

main 함수를 보시죠. Child class 객체를 생성하고 delete 합니다.

즉 저의 목적은 Child class를 생성하는 것입니다. 이때 순서는 상속받은 Parent class의 생성자부터 불린다는 점입니다. 즉 Parent 가 먼저 생성되고 나서 Child 가 생성되는 것이죠. (실제 parent와 child를 생각해 보시면 당연하겠죠)

 

예제 결과

 

그런 다음 Child가 먼저 소멸되고 Parent 가 그 다음 소멸됩니다.

즉 Base가 되는 Parent가 Child를 포함하고 있다? 감싸고 생각하면 될 것 같습니다.

 

 가상 함수 추가

 

아래 WhoIsIt 가상 함수를 추가했습니다. Parent에 가상함수가 있고, Child에도 있습니다.

main에서 Child 에 관심이 있고, 애초에 목적은 child가 생성될 때 I'm child를 출력하고 싶었습니다. 

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent() {
        cout << "Parent construct" << endl;
        WhoIsIt();
    }
    ~Parent() {
        cout << "Parent destruct" << endl;
    }

    virtual void WhoIsIt() {
	    cout << "I'm parent" << endl;
    }
};

class Child : public Parent
{
public:
    Child() {
        cout << "Child construct" << endl;
    }
    ~Child() {
        cout << "Child destruct" << endl;
    }

    void WhoIsIt() override {
	    cout << "I'm child" << endl;
    }
};

int main()
{
    auto* child = new Child;
    delete child;
}

 

결과는 어떨까요?

예제 결과

 

I'm parent 가 출력되네요. 사실 예제 코드만 보면 당연하다고 느낄 수 있겠지만 main과, 의도를 생각하면 뭔가 모호합니다. 즉 가상함수의 목적이 대체로 구현부의 override 된 함수를 사용하고자 하는 것일 텐데, 의도와는 다르게 동작하는 것 같습니다.

여기 원인이 바로 Parent의 생성자가 호출될 시점에는 Child가 생성되어 있지 않아서 입니다.

 

따라서 생성자에서 가상함수를 호출하는 것은 가상함수를 override 하는 목적과 의도와는 상관없이 항상 해당 멤버함수와 가은 역할을 하므로 일반적으로 사용하지 않는 것이 좋을 것 같습니다.

 

만약 순수 가상함수로 만들면 어떻게 될까요? Child의 WhoIsIt이 호출될까요?

class Parent
{
public:
    Parent() {
        cout << "Parent construct" << endl;
        WhoIsIt();
    }
    ~Parent() {
        cout << "Parent destruct" << endl;
    }

    virtual void WhoIsIt() = 0;
};

 

실행해 보면 빌드에 실패합니다. 원인은 Parent::WhoIsIt()을 알 수 없다고 나옵니다. 즉 Parent 가 생성되면서 호출하는 WhoIsIt 함수를 Parent 내 멤버함수로 링킹을 하려다가 실패하는 것입니다. 즉 애초에 빌드타임에 가상함수가 아닌 멤버함수처럼 취급이 된다는 걸 알 수 있습니다.

순수가상 함수 실패

 

 

 결론

 

오늘은 생성자에서 가상함수를 사용하는 경우에 대해서 알아보았습니다. 코드는 결국 사람이 읽고 작성하고 수정합니다. 따라서 코드의 모호함은 가독성을 떨어뜨리고 잠재적인 버그를 양산할 수 있는 가능성이 있으므로 최대한 제거하는 것이 좋을 것 같습니다. 

반응형