[Effective C++]상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!

상속, 그리고 객체 지향 설계, 다섯 번째 이야기

Posted by SungBeom on December 24, 2019 · 4 mins read

같은 객체, 같은 함수, 다른 동작

D라는 이름의 클래스가 B라는 이름의 클래스로부터 public 상속에 의해 파생되었고, B 클래스에는 mf라는 이름의 public 멤버 함수가 정의되어 있다고 가정합시다. 매개변수나 반환 타입은 둘 다 void라고 해 둡시다.

1
2
3
4
5
6
7
class B {
public:
    void mf();
    ...
};
 
class D: public B { ... };
cs

D 타입으로 x라는 객체를 생성하고, x 객체에 대한 포인터를 얻어, mf 멤버 함수를 호출할 경우에 상황에 따라 동작이 다를 수 있습니다. 함수도 똑같고 객체도 똑같으니, 동작도 같아햐 하는 것이 이치적으로 맞지만, 다를 수도 있다는 게 문제입니다. 특히, mf가 비가상 함수이고 D 클래스가 자체적으로 mf 함수를 또 정의하고 있으면 위와 같은 황당한 동작이 나오게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class D: public B {
public:
    void mf();  // B::mf를 가려 버립니다.
    ...
};
 
D x;            // x는 D 타입으로 생성된 객체입니다.
 
*pB = &x;     // x에 대한 포인터를 얻어냅니다.
*pD = &x;     // x에 대한 포인터를 얻어냅니다.
 
pB->mf();       // B::mf를 호출합니다.
pD->mf();       // D::mf를 호출합니다.
cs

이렇게 두 얼굴의 동작을 하는 이유는, B::mf 및 D::mf 등의 비가상 함수는 정적 바인딩(static binding)으로 묶이기 때문입니다. 무슨 뜻인고 하니, pB는 'B에 대한 포인터' 타입으로 선언되었기 때문에, pB를 통해 호출되는 비가상 함수는 항상 B 클래스에 정의되어 있을 것이라고 결정해 버린다는 말입니다. 심지어 B에서 파생된 객체를 pB가 가리키고 있다 해도 마찬가지입니다.

반면 가상 함수의 경우엔 동적 바인딩(dynamically binding)으로 묶입니다. 비가상 함수와 같은 문제로 골머리를 썩을 이유가 없죠. 만약 mf 함수가 가상 함수였다면, mf가 pB에서 호출되든 pD에서 호출되든 D::mf가 호출됩니다. pB 및 pD가 진짜로 가리키는 대상은 D 타입의 객체이니까요.

만약 D 클래스를 B로부터 파생시켜 만드는 도중에 B 클래스로부터 물려받은 비가상 함수인 mf를 재정의해 버리면, D 클래스는 일관성 없는 동작을 보이는 이상한 클래스가 됩니다. 특히, 분명히 D 객체인데도, 이 객체에서 mf 함수를 호출하면 B와 D 중 어디로 튈지 모르는 결과를 낳게 됩니다. 게다가 B냐 D냐를 좌우하는 요인이 해당 객체 자신이 아니라, 그 객체를 가리키는 포인터의 타입이란 점이 심각하게 암울합니다. 참조자도 좌절스런 문제를 일으키기는 포인터나 마찬가지입니다.

D에서 mf를 재정의하면 public 상속의 의미인 "is-a(...는 ...의 일종이다)" 설계 중 '모든 D는 B의 일종'이란 명제에 모순이 생겨 버립니다. 만약 mf를 B와 다르게 구현한 것이 진짜로 원해서 그런 거라면, D는 B로부터 public 상속을 받으면 안 됩니다. 한편, D는 B로부터 public 상속을 받아 파생시킬 수밖에 없는 사정이 있고, 진짜로 D에서 mf 함수를 B의 그것과 다르게 구현해야 한다면, 'mf는 클래스 파생에 상관없이 B에 대한 불변동작을 나타낸다'라는 점도 참이 되지 않습니다. 이런 경우라면 mf는 가상 함수로 만들어지는 것이 맞습니다. 마지막으로, 만약 모든 D가 B의 일종이고 정말 mf가 클래스 파생에 상관없는 B의 불변동작에 해당한다면, D에서는 결단코 mf를 재정의할 생각도 할 수 없습니다.

실제적이든 이론적이든, 받아들이는 입장에서는 문제일 수밖에 없습니다. 어떤 상황에서도 상속받은 비가상 함수를 재정의하는 것은 절대 금물입니다.


정리

상속받은 비가상 함수를 재정의하는 일은 절대로 하지 맙시다.