Programming Language/C++

[C++] 다중 상속과 다이아몬드 문제

lumana 2024. 6. 28. 17:44

다중 상속과 다이아몬드 문제

  • 한 클래스가 여러 부모를 가지고 있는 경우를 다중 상속이라고 한다

    • cf) 자바와 C#은 다중 상속을 허용하지 않음
#include <iostream>
using namespace std;

struct Mom {
    int a = 1;
};

struct Dad {
    int b = 2;
};

struct Child : Mom, Dad {
    int c = 3;
};

int main() {
    Child ch;
    cout << ch.a << endl;
    cout << ch.b << endl;
    cout << ch.c << endl;
}
  • 여기까지는 문제가 없다.

  • 만약 Dad의 멤버 변수가 b가 아니라 a라면?

#include <iostream>
using namespace std;

struct Mom {
    int a = 1;
};

struct Dad {
    int a = 2;
};

struct Child : Mom, Dad {
    int c = 3;
};

int main() {
    Child ch;
    cout << ch.Mom::a << endl;
    cout << ch.Dad::a << endl;
    cout << ch.c << endl;
}
  • Mom으로부터 상속받은 a와 Dad로부터 상속받은 a 두가지가 있다.

    • 어느 클래스로 부터 상속 받는 객체인지 표기해줘야 한다.

    • ch.Mom::a

  • 만약 다음과 같은 구조로 설계되어있다면?

#include <iostream>
using namespace std;

struct Person {
    int a;
};

struct Mom : Person {
    Mom() {
        a = 1;
    }
};

struct Dad : Person {
    Dad() {
        a = 2;
    }
};

struct Child : Mom, Dad {
    int c = 3;
};

int main() {
    Child ch;
    cout << ch.Mom::a << endl;
    cout << ch.Dad::a << endl;
    cout << ch.c << endl;
}
  • 이 예시에서는 문제가 없지만, 이런 다중 상속 구조에는 큰 문제가 존재한다

  • 아래 예시를 봐보자

#include <iostream>
using namespace std;

struct Person {
    int age;
    void Eat() {
        cout << "먹는다..." << endl;
    }
};

struct Student : Person {
    void Study() {
        cout << "공부한다..." << endl;
    }
};

struct Worker : Person {
    void Work() {
        cout << "일한다..." << endl;
    }
};

struct Researcher : Student, Worker {

};

int main(void) {
    Researcher r;
    // r.age는 불가능
    r.Student::age = 10;
    r.Worker::age = 20;
        // r.eat()는 불가능
        r.Student::Eat();
    r.Worker::Eat();
}
  • 사람의 나이 : 학생으로서의 나이와, 직장인으로서의 나이 두 가지가 존재하게 된다

  • Eat() 또한 두 가지가 존재하게 된다. r.eat()처럼 그냥 먹을 수가 없음

  • 위 예시를 다형적 클래스 구조로 바꿔보자

#include <iostream>
using namespace std;

struct Person { // 다형적 클래스
    int age;
    virtual ~Person() {}
    void Eat() {
        cout << "먹는다..." << endl;
    }
};

struct Student : Person {
    void Study() {
        cout << "공부한다..." << endl;
    }
};

struct Worker : Person {
    void Work() {
        cout << "일한다..." << endl;
    }
};

struct Researcher : Student, Worker {

};

int main(void) {
    // Person *p = new Researcher;는 불가능
}
  • Researcher는 Person으로 부터 직접 상속 받은 것이 아니기 때문에 Person 포인터로 가리킬 수 없다

    • 이런 상황에서는 다형성도 사용할 수 없는 단점이 존재한다

    • 이런 문제를 다이아몬드 문제라고 한다

  • C++에서는 다이아몬드 문제를 방지하기 위해 가상 상속을 제공함

    • 가상 상속을 사용하게 되면 Researcher는 Stduent, Worker로부터가 아니라 Person으로부터 직접 상속을 받게 된다
#include <iostream>
using namespace std;

struct Person { // 다형적 클래스
    int age;
    virtual ~Person() {}
    void Eat() {
        cout << "먹는다..." << endl;
    }
};

struct Student : virtual Person {
    void Study() {
        cout << "공부한다..." << endl;
    }
};

struct Worker : virtual Person {
    void Work() {
        cout << "일한다..." << endl;
    }
};

struct Researcher : Student, Worker {

};

int main(void) {
    Researcher r;
    r.age = 10;
    r.Eat();
}
  • 이러한 방식에도 한계가 있다.

    • 만약 Eat() 멤버 함수를 오버라이딩 하게 되면 가상 상속을 했다 하더라도 r.Eat();에서 에러가 발생한다
  • "다중 상속은 인터페이스로부터만 받는다"라는 약속을 해야 한다

    • 인터페이스 : 모든 메서드가 순수 가상 함수이고 (비정적)멤버 변수는 없는 클래스

    • cf) 추상 클래스 : 순수 가상 함수가 하나 이상 들어있는 클래스

    • cf) 다형적 클래스 : 가상 함수가 하나 이상 들어 있는 클래스

    • cf) 자바에서도 인터페이스가 존재함

  • 부모 클래스를 인터페이스로 바꿔보자

#include <iostream>
using namespace std;

struct IPerson { // 다형적 클래스
    virtual ~IPerson() {}
    virtual void Eat() = 0;
};

struct IStudent : virtual IPerson {
    virtual void Study() = 0;
};

struct IWorker : virtual IPerson {
    virtual void Work() = 0;
};

struct Researcher : IStudent, IWorker {
    void Eat() {
        cout << "먹는다... " << endl;
    }
    void Study() {
        cout << "공부한다... " << endl;
    }
    void Work() {
        cout << "일한다... " << endl;
    }
};

int main(void) {
    Researcher r;
    r.Eat();
}
  • IStudent와 IWorker 인터페이스는 IPerson으로 부터 기존처럼 가상 상속을 받아야 한다

  • Researcher는 Eat()를 IWorker와 IStudent가 아닌 IPerson으로 부터 상속을 받는다

    • 이전에 인터페이스가 아닌 클래스로 구현했던 코드에서는, Person의 Eat()가 실체가 있는 함수였기 때문에 오버라이딩을 하게 되면 문제가 발생했었다.

      • 하지만 인터페이스에서는 실체가 없는 상황이 되었으므로 Researcher는 Person으로부터 안전하게 상속을 받을 수 있다.
  • age를 처리하고 싶은데, 인터페이스에서는 비정적 멤버 변수를 포함할 수 없다고 했다. 어떻게 해야할까?

    • Researcher 클래스에 age를 선언해줘야 한다

      • IPerson 포인터를 사용해 다형성을 이용하고자 하는 경우 age 멤버 변수를 사용할 수 없게 된다.
    • 이를 해결하기 위해 IPerson 인터페이스에 virtual int GetAge() = 0;이라는 순수 가상 함수를 만들어 처리할 수 있다.

      • 물론 상속성에는 어긋나는 행동이긴 하다. Researcher2라는 클래스를 만들게 되면 이 클래스 내에 age 멤버 변수를 또 한 번 선언해줘야 한다

      • 하지만 이 방법밖에 없다.

#include <iostream>
using namespace std;

struct IPerson { // 다형적 클래스
    virtual ~IPerson() {}
    virtual void Eat() = 0;
    virtual int GetAge() = 0;
};

struct IStudent : virtual IPerson {
    virtual void Study() = 0;
};

struct IWorker : virtual IPerson {
    virtual void Work() = 0;
};

struct Researcher : IStudent, IWorker {
    void Eat() {
        cout << "먹는다... " << endl;
    }
    void Study() {
        cout << "공부한다... " << endl;
    }
    void Work() {
        cout << "일한다... " << endl;
    }
    int GetAge() {
        return age;
    }
    int age;
};

int main(void) {
    Researcher r;
    r.age = 10;
    r.Eat();
}
  • Student 타입을 갖는 인스턴스를 만들고 싶다면?

    • IStudent 인터페이스로부터 하위 클래스 Student를 만들어서 처리한다

    • Worker의 경우도 마찬가지

#include <iostream>
using namespace std;

struct IPerson { // 다형적 클래스
    virtual ~IPerson() {}
    virtual void Eat() = 0;
    virtual int GetAge() = 0;
};

struct IStudent : virtual IPerson {
    virtual void Study() = 0;
};

struct Student : IStudent {
    void Eat() {
        cout << "먹는다... " << endl;
    }
    void Study() {
        cout << "공부한다... " << endl;
    }
    int GetAge() {
        return age;
    }
    int age;
};

struct IWorker : virtual IPerson {
    virtual void Work() = 0;
};

struct Researcher : IStudent, IWorker {
    void Eat() {
        cout << "먹는다... " << endl;
    }
    void Study() {
        cout << "공부한다... " << endl;
    }
    void Work() {
        cout << "일한다... " << endl;
    }
    int GetAge() {
        return age;
    }
    int age;
};

int main(void) {
    IPerson *p = new Researcher;
    p->GetAge();
    delete p;
    p = new Student;
    p->GetAge();

    Student s;
    s.Study();
    Researcher r;
    r.Study();
    r.Work();
}
  • 다중 상속에서는 상속성에 위배되고, 코드의 중복이 발생하는 경우가 많다

    • 이런 점이 객체 지향의 한계?라고 볼 수 있다