어떤 함수에 대해서도 상속받은 기본 매개변수 값은

절대로 재정의하지 말자.


가상 함수는 동적으로 바인딩되지만,

기본 매개변수 값은 정적으로 바인딩된다.




class AAA{

public:

enum num { first , second , third };

virtual void show(num n = first);

}


class BBB : public AAA{

public:

virtual void show(num n = second);

}


class CCC : public AAA{

public:

virtual void show(inum n);

}


AAA *pa;

AAA *pb = new BBB;

AAA *pc = new CCC;



여기서 pb, pc는 모두 AAA에 대한 포인터로

선언되었기 때문에, 각각의 정적 타입도 모두 이 타입니다.

객체의 동적 타입은 현재 그 객체가 진짜로 무엇이냐에

따라 결정되는 타입니다. 다시 말해, '이 객체가 어떻게

동작할 것이냐'를 가리키는 타입이 동적 타입이다.




따라서 pb의 동적타입은 BBB*

pc의 동적 타입은 CCC*

pa는 동적 타입이 없다.




pb->show();

문제는 이 가상함수를 호출했을 때 발생한다.

파생 클래스에 정의된 가상 함수를 호출하면서

기본 클래스에 정의된 기본 매개변수 값을

사용해 버릴 수 있다는 것이다.


pb의 동적타입은 BBB*이기 때문에

함수는 BBB::show가 호출되지만

기본 매개변수 값은 AAA에서 가져온다.




이러한 동작방식은 런타임 효율이라는 요소가 숨어있다.

프로그램 실행 중에 가상 함수의 기본 매개변수 값을

결정할 방법을 컴파일러 쪽에서 마련해 주어야 하는데,

이 방법은 컴파일 과정에서 결정하는 현재의 메커니즘보다

느리고 복잡하다. 때문에 속도와 구현 간편성에 무게를

더 두어야 했고, 그 덕에 효율 좋은 실행 동작을 누릴 수 있게 되었다.




그렇다면 원하는 대로 가상함수가 동작하도록 만들어보자.

그 전 포스팅에서도 언급했던 NVI 관용구를 써보자.


class AAA{

public:

enum num { first , second , third };


void show ( num n = first ) const {

doShow(n);

}


private:

virtual void doShow(num n) const = 0;

}


class BBB : public AAA{

public:

...

private:

virtual void doShow(num n) const;

};




비가상 함수는 파생 클래스에서 오버라이드되면 안 되기 떄문에

위와 같이 설계한다면, 기본 매개변수에 대한 문제를 해결할 수 있다.


POINT!!

상속받은 기본 매개변수 값은 절대로 재정의해서는 안된다.

기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수는

동적으로 바인딩되기 때문이다.


[C++] 상속받은 비가상 함수의 재정의는 절대 금물!!


앞선 포스팅에서 언급했듯이

비가상 함수의 상속은 파생에 관계없는

불변동작을 정해두는 것이다.

인터페이스와 필수 구현을 물려주는 것

그 것이 바로 비가상 함수의 상속인 것이다.




양쪽에서 x의 객체로 부터 mf 함수를 호출한다.

함수도 똑같고 객체도 똑같으니, 동작 또한 같다.

하지만 D클래스가 mf 함수를 또 정의하면 어떻게 될까.





이렇게 두 개의 다른 동작이 나오는 이유는

비가상 함수는 정적 바인딩이기 때문이다.

pb는 B에 대한 포인터 타입으로 선언되었기 때문에

pb를 통해 호출되는 비가상 함수는 항상 B클래스에

정의되어 있을 것이라고 결정해 버린다.


마찬가지로 B에서 파생된 객체를 pb가

가리키고 있다 해도 결과는 마찬가지이다.




반면, 가상 함수는 동적바인딩으로 묶이는데

mf가 가상 함수 였다면, mf가 어디서

호출 되든 D::mf가 호출된다.

pb, pd가 진짜로 가리키는 대상은 D 타입 객체이기 때문이다.




즉, 비가상 함수의 상속에서는

B냐 D냐를 결정하는 요인이 해당 객체가 아니라

그 객체를 가리키는 포인터 타입이라는 것이다.




이제는 비가상 함수의 재정의를 하면 안되는

이론적인 이유에 대해서 알아보도록 하자.


비가상 멤버 함수는 클래스 파생에 관계없는

불변동작을 정해두는 것이다.

is-a 관계 상속(public)의 관점에서 보자면




1. B 객체에 해당되는 모든 것들이 D 객체에 그대로 적용된다.

2. B에서 파생된 클래스는 mf 함수의 인터페이스와 구현을 모두 물려받는다.


D에서 mf를 재정의 하는 순간 이러한 설계는 모순이 생겨 버린다.

B의 mf 구현을 모든 파생 클래스에서 사용하도록 정한 것인데

mf를 D에서 재정의 하는 순간 모든 D는 B의 일종이라는

명제는 사라져 버린다. D에서 정말 mf함수를 다르게

구현해야 한다면 비가상 함수가 아닌 가상 함수여야 한다.



POINT!!

상속받은 비가상 함수를 재정의하는 일은 절대로 하지 말자!!



[C++] 오버로딩 된 함수의 상속, using 선언


기본 클래스와 똑같은 이름의 멤버 함수를

파생 클래스에서 정의할 경우(비가상으로)

바깥쪽 유효범위에 있는 이름을 가리는

특성으로 인해 기본 클래스의 함수가 숨겨져 버린다.

( 참고로 비가상 함수를 파생클래스에서 재정의

하는 일은 절대로 해서는 안된다. )




하지만 때때로 가상 함수의 이름도 가려져 버리는

경우가 있는데, 기본 클래스로부터 오버로드

버전을 상속시키는 경우이다. 컴파일러는

기본 클래스의 오버로드 버전 상속을 막고 있는데,

public 상속을 쓰면서 이러한 상속을 하지 못하는 것은

엄연히 is-a 관계 위반이라고 할 수있다. 방법이 없을까?




비가상 함수인지 가상 함수인지 여부에

상관없이 이름이 가려진다.





파생 클래스 내에 using 선은을 해줌으로써

가려진 이름을 끄집어 냈다.


기본 클래스로부터 상속을 받으려고 하는데,

오버로드된 함수가 그 클래스에 들어 있고

이 함수 중 몇개만 재정의하고 싶다면,

각 이름에 대해 using 선언을 붙여 주어야 한다.


이렇게 하지 않으면 이름이 가려져 버린다.




POINT!!

가려진 이름을 다시 볼 수 있게 하는 방법으로,

using 선언을 쓸 수 있다.


+ Recent posts