객체지향프로그래밍(21) - 가상 상속

가상 상속이란?

Featured image

🔚 짧게 하는 복습

✅ 1. 가상 소멸자의 정의와 활용을 안다.

✅ 2. 가상 소멸자 이용 시 소멸자 호출 순서를 안다.

혹시 기억이 안 난다면, 다시 돌아가자


어느덧 이 객체지향 프로그래밍의 챕터도 끝이 가까워지고 있다.

여기까지 따라오느라 정말 고생이 많았을 듯하다.

오늘은 조금 쉬어가는 챕터로 가상 상속라는 것을 다룰 것이다.


가상 상속이란?

여러분들은 이제 눈치를 챘겠지만, virtual이라는 키워드는 실존하지 않는다는 뉘앙스가 아니다.

일단 임시로 새워놓고 상황이 발생하면 확인하겠다는 동적인 자세가 더 크다.

그런 의미에서 가상 상속도 비슷하다.

일단 상속하겠다고 정의는 했지만, 문제가 되는 경우 그때 가서 하지 않겠다는 것이 가상 상속이다.

이게 왜 필요할까? 상속하겠다고는 했지만, 실제로 상속을 하지 않는 경우가 생긴다는 것일까?

가상 상속을 이해하려면 다시 다이아몬드 문제로 돌아가야 한다.


다시 돌아온 다이아몬드 문제

class A {
public:
    A() { std::cout << "A constructor called" << std::endl; }
};

class B : public A {
public:
    B() { std::cout << "B constructor called" << std::endl; }
};

class C : public A {
public:
    C() { std::cout << "C constructor called" << std::endl; }
};

class D : public B, public C {
public:
    D() { std::cout << "D constructor called" << std::endl; }
};

이 코드 굉장히 익숙하다. 15강에서 다룬 다중 상속의 대표적인 문제, 다이아몬드 문제이다.

이 경우 메인 문제가 뭐였는지 기억하는가?

바로 B와 C의 상위 클래스인 A가 두 개나 만들어진다는 것이다.

그래서 D에서 A의 메소드를 호출하면, 어떤 A의 메소드를 따라가야 하는지 문제가 생겼다.

이 경우 virtual을 쓰면 최소한 이 문제의 반은 해결할 수 있다.

바로 B와 C가 가상 상속을 하는 것이다.

class A {
public:
    A() { std::cout << "A constructor called" << std::endl; }
};

class B : virtual public A {
public:
    B() { std::cout << "B constructor called" << std::endl; }
};

class C : virtual public A {
public:
    C() { std::cout << "C constructor called" << std::endl; }
};

class D : public B, public C {
public:
    D() { std::cout << "D constructor called" << std::endl; }
};

이렇게 하면 D에서 B를 먼저 호출하든 C를 먼저 호출하든 먼저 호출된 쪽에서 A를 만들고, 후에 호출된 쪽에서는 A가 호출되지 않는다.

이렇게 가상 상속을 통해서 다이아몬드 문제를 해결해보았다!

하고 오늘 배울 내용은 여기까지가 끝이면 좋겠지만…


가상 상속으로는 다이아몬드 문제를 해결할 수 없다.

안타깝게도 가상 상속으로도 다이아몬드 문제를 완전히 해결할 수는 없다.

지금 위에서는 문제가 될 것이 없지만, D에서 어느 쪽이 먼저 호출되는지 모른다는 것은 여전히 문제의 가능성을 남긴다.

아래의 코드를 보자.

class A {
private:
    int data;
public:
    A(int _data) : data(_data) { std::cout << "A constructor called" << std::endl; }
};

class B : virtual public A {
public:
    B() : A(10) { std::cout << "B constructor called" << std::endl; }
};

class C : virtual public A {
public:
    C() : A(20) { std::cout << "C constructor called" << std::endl; }
};

class D : public B, public C {
public:
    D() { std::cout << "D constructor called" << std::endl; }
};

이 코드는 다이아몬드 문제가 여전히 남아있다는 것을 보여주는 코드이다.

D->B->A 순으로 먼저 호출되면 data에는 10이 남는다.

반면에 D->C->A 순으로 호출되면 data에는 20이 남는다.

그렇기에 이 구조에는 여전히 문제가 있다.

하고 오늘 배울 내용은 여기까지가 끝이면 좋겠지만….


진짜 진짜 마지막

사실 가상 상속으로 다이아몬드 문제를 완전히 해결할 수 있다!

그 이유는 생성자의 호출 순서가 정해져 있기 때문이다.

아래 오피셜 문서를 보자. (원본 출처는 더 읽어보기에 남기겠다.)

The very first constructors to be executed are the virtual base classes anywhere in the hierarchy. They are executed in the order they appear in a depth-first left-to-right traversal of the graph of base classes, where left to right refer to the order of appearance of base class names.

After all virtual base class constructors are finished, the construction order is generally from base class to derived class. The details are easiest to understand if you imagine that the very first thing the compiler does in the derived class’s ctor is to make a hidden call to the ctors of its non-virtual base classes (hint: that’s the way many compilers actually do it). So if class D inherits multiply from B1 and B2, the constructor for B1 executes first, then the constructor for B2, then the constructor for D. This rule is applied recursively; for example, if B1 inherits from B1a and B1b, and B2 inherits from B2a and B2b, then the final order is B1a, B1b, B1, B2a, B2b, B2, D.

Note that the order B1 and then B2 (or B1a then B1b) is determined by the order that the base classes appear in the declaration of the class, not in the order that the initializer appears in the derived class’s initialization list.

가상 상속에서는 상속된 가상 기반 클래스들의 생성자가 우선적으로 호출됩니다. 이들은 상속된 순서와 관계 없이, 상속 그래프를 깊이 우선으로 왼쪽에서 오른쪽으로 탐색한 순서대로 호출됩니다. 그 다음으로, 일반적으로 기본 클래스에서 파생 클래스로의 생성자 호출이 순차적으로 이루어집니다. 파생 클래스의 생성자가 실행되기 전에는 기본 클래스들의 생성자가 모두 호출되어야 합니다.

요약해서, 생성자의 일반적인 호출 순서는 정해져있다는 것이다.

그렇기에 C++에서는 가상 상속을 통해서 완벽하게 다이아몬드 문제를 해결할 수 있다.


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. 가상 상속의 정의와 활용을 안다.

✅ 2. 가상 상속을 통해 다이아몬드 문제의 부분 해결을 할 수 있다.

✅ 3. 하지만 가상 상속을 해도 여전히 남아있는 문제에 대해서 안다.

✅ 4. 그러나 왜 C++에서 해결될 수 있었는지 알아본다.

⚠️ 다이아몬드 구조를 다룰 때 각 생성자끼리 최대한 충돌이 나지 않게 조심해서 다루어야 한다.

⚠️ 읽어볼 거리가 참 유익하다. 꼭 읽어봤으면 좋겠다.

💣 과제, 없음

🔜 더 공부해보기,

읽어볼 거리(1) - 다중 상속 및 가상 상속의 공식 문서