본문 바로가기

Effective C++/6. 상속, 그리고 객체 지향 설계

항목 36: 상속받은 비가상 함수를 파생 클래스에 재정의하는 것은 절대 금물!

D라는 이름의 클래스가 B라는 이름의 클래스로부터 public 상속에 의해 파생되었고, B 클래스에는 mf()라는 public 맴버 함수가 정의되어 있다고 가정해보자.

 

 

 

 

 

여기서의 pB->mf()

 

 

당연히 이런식으로 동작할 것이라고 예상할 있다. ? 같은 객체인 x 대해 mf() 호출하고 있기 때문이다.

 

하지만 당연하지 않을 수도 있다는 것이 문제이다. 특히

  1. mf() 비가상 함수이고
  2. D 클래스가 자체적으로 mf() 함수를 정의하고 있는 경우

 

아래 코드와 같은 황당한 일이 벌어진다.

 

 

 

이렇게 같은 객체에서 같은 함수를 호출하는데, 실제로는 다른 함수를 호출하는 이유는, 비가상 함수는 정적 바인딩(static binding)[각주:1]으로 묶이기 때문이다.(항목 37 참고)

 

=> pB 가르키는 실제 객체가 아닌, pB 참조타입만 보고 문장을 CALL B::mf() 바꿔버린다.(컴파일 타임에 모든게 결정이 된다.)

 

반면, 가상 함수의 경우 동적 바인딩(dynamically binding)으로 묶인다. CALL 주소를 가상 함수 테이블을 보고 결정한다. 만약 mf() 가상 함수였다면 mf pB에서 호출되든 pD에서 호출되든 D::mf() 호출된다. pB pD 가리키는 객체(D) 가상 함수 테이블을 보고 결정하기 때문이다.

 

만약 우리가 D 클래스(B로부터 파생된) 만드는 도중에 B 클래스로 부터 무려받은 비가상 함수인 mf() 재정의해버리면(오버라이드 해버리면), D 클래스는 일관성 없는 동작을 보이는 이상한 클래스가 된다. 특히 분명히 D 객체인데도, 객체에서 mf() 호출하면 B D 어디의 mf() 호출될 모른다는 소리다.(실제 객체가 아닌 단순 참조타입에 의해 결정됨)

 

항목 32 읽은 사람은 알겠지만, public 상속 = is-a(~ ~ 일종이다.)  이고, 항목 34 의하면 비가상 맴버함수는 클래스 파생에 관계없는 불변동작(필수 구현) 정해 두는 것이다. 두가지 포인트를 B, D 클래스 비가상 맴버 함수인 B::mf() 그대로 가져가면, 다음과 같은 결론이 나온다.

 

  • B 객체에 해당되는 모든 것들이 D 객체에 그대로 적용된다. 왜냐하면 모든 D 객체는 B 객체의 일종이기 때문이다.
  • B에서 파생된 클래스는 mf() 인터페이스와 필수 구현을 모두 물려받는다 mf() B 클래스에서 비가상 맴버 함수이기 때문이다.

 

결론에 도달한 다시 D에서 mf() 재정의한다고 생각해보자. 흠… 뭔가 모순이 생긴다.

  • 만약 D::mf() B 다르게 구현한 것이 진짜로 원해서 그런 것이였고
  • B B 파생 클래스로부터 만들어진 모든 객체가 B mf() 구현을 사용해야 한다고 정한 것이 진짜라면

=> mf() 재정의로 인해 "모든 D B 일종이다" 명제는 바로 거짓이 되어버린다.

=> 이런 상황이라면 D B로부터 public 상속을 받으면 안된다.

 

한편, D B로부터 public으로 밖에 파생시킬 없는 사정이 있고, 진짜로 D::mf() B::mf() 다르게 구현해야 한다면, "mf() 클래스 파생에 상관없이 B 대한 불변동작을 나타낸다." 라는 명제도 거짓이 된다. 이런 경우라면 mf() 가상함수로 만드는 것이 맞다.

 

마지막으로, 만약 모든 D B 일종이고 정말 mf() 클래스 파생에 상관없는 B 불변동작에 해당된다면( mf() 어떠하게 파생된 경우에도 똑같은 동작을 수행함), D에서는 절대로 mf() 재정의할 생각도 없다.

 


 

항목 7에서, 다형성을 부여한 기본 클래스의 소멸자는 반드시 가상 함수로 만들어 두어야 한다고 했는데, 말대로 하지않으면(, 다형성 기본 클래스에서 비가상 소멸자를 선언해버리면) 항목을 제대로 공부하지 않은 것이다. 분명히 상속받은 비가상 함수(비가상 소멸자) 절대 재정의되면 안된다고 했는데, 파생 클래스에서는 상속받은 비가상 함수(비가상 소멸자) 재정의할 것이 뻔하기 때문이다. 심지어 소멸자를 선언하지 않아도 이런 문제가 발생한다.(항목 5 참고)

 


  • 상속받은 비가상 함수를 재정의하는 일은 절대로 하지 맙시다.
  1. 바인딩이란, 프로그램 소스에 쓰인 각종 내부요소, 이름, 식별자들에 대해 혹은 속성을 확정하는 과정을 일컫는다. 과정이 빌드 중에 일어지면 정적 바인딩이라고 하고, 런타임에 이루어지면 동적 바인딩이라고 한다. 예를 들어, "int foo = 2;" 라는 문장이 있을 , 데이터 타입이 int 정해지는 것과 타입의 변수명이 foo 정해지는 것은 정적 바인딩이고, foo 변수에 2 대입 되는 것은 동적 바인딩이다. C++ 가상 함수의 바인딩은 문서상으로는 동적 바인딩이라고 되어 있으나, 구현상으로는 런타임 성능을 높이기 위해 정적 바인딩을 쓰고 있다. 컴파일 중에 아예 가상 함수 테이블을 파생 클래스에 맞게 바꿈으로써, 겉보기에는 파생 클래스 타입에서 오버라이드한 가상 함수를 호출하는 것처럼 보이게 만드는 것이다.

    [본문으로]