[Effective C++] 37. 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
바인딩(binding)
프로그램 소스에 쓰인 각종 내부 요소, 이름, 식별자들에 대해 값 혹은 속성을 확정하는 과정을 일컫는다. 이 과정이 빌드 중에 이루어지면 정적 바인딩이라고 하고, 실행 중에 이루어지면 동적 바인딩이라고 한다.
공식적으로, 정적 바인딩은 선행 바인딩이란 다른 이름으로도 알려져 있고 동적 바인딩은 지연 바인딩이란 이름으로도 알려져 있다.
기본 매개변수 값을 가진 가상 함수를 상속하는 경우
가상 함수는 동적으로 바인딩되지만, 기본 매개변수 값은 정적으로 바인딩된다.
객체의 정적 타입(static type)은 프로그램 소스 안에 놓는 선언문을 통해 그 객체가 갖는 타입이다.
아래의 클래스 계통을 보자.
class Shape
{
public:
enum ShapeColor( Red, Green, Blue );
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle: public Shape
{
public:
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle: public Shape
{
public:
virtual void draw(ShapeColor color) const;
...
};
---
Shape *ps; // 정적 타입 = Shape*
Shape *pc = new Circle; // 정적 타입 = Shape*
Shape *pr = new Rectangle; // 정적 타입 = Shape*
여기서 ps, pc, pr은 모두 Shape에 대한 포인터로 선언되어 있기 때문에 각각의 정적 타입도 모두 이 타입이다. 단, 그렇다고 해서 진짜로 가리키는 대상이 달라지는 것은 하나도 없다. 그냥 정적 타입이 Shape* 일 뿐이다.
객체의 동적 타입은 현재 그 객체가 진짜로 무엇이냐에 따라 결정되는 타입이다. 즉, 이 객체가 어떻게 동작할 것이냐를 가리키는 타입이 동적 타입이라 한다. 위의 예시로 보면, pc의 동적 타입은 Circle*이고, pr의 동적 타입은 Rectangle*, ps의 경우엔 아직 아무 객체도 참조하지 않았기 때문에 동적 타입이 없다.
가상 함수는 동적으로 바인딩되기 때문에 호출이 일어난 객체의 동적 타입에 따라 어떤 가상 함수가 호출될지가 결정된다.
pc->draw(Shape::Red); // Circle::draw(Shape::Red)를 호출함
pr->draw(Shape::Red); // Rectangle::draw(Shape::Red)를 호출함
문제점
가상 함수는 동적으로 바인딩되어 있지만 기본 매개변수는 정적으로 바인딩되어있다.
만약 함수의 기본 매개변수가 동적으로 바인딩된다면, 프로그램 실행 중에 가상 함수의 기본 매개변수 값을 결정할 방법을 컴파일러 쪽에서 마련해 주어야하기 때문에 컴파일 과정에서 결정하는 현재의 메커니즘보다는 느리고 복잡하기 때문에 런타임 효율을 위해 위의 실행 방법을 선택하게 된 것이다.
여기서의 문제는 위처럼 기본 매개변수 값을 가상 함수를 상속하고 파생 클래스에서 매개변수 값을 정의한다면, 파생 클래스에 정의된 가상 함수를 호출하면서 기본 클래스에 정의된 기본 매개변수 값을 사용해버릴 수 있다.
즉, 동작이 이상해질 수 있다.
그럼 기본 매개변수 값을 똑같이 제공한다면 무엇이 문제가 될 수 있을까?
class Shape
{
public:
enum ShapeColor( Red, Green, Blue );
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle: public Shape
{
public:
virtual void draw(ShapeColor color = Red) const;
...
};
여기서의 단점은 코드 중복에 의존성까지 걸려있다는 점이다.
만약 Shape클래스에서 기본 매개변수 값이 변하기라도 한다면 그 값을 가진 파생 클래스는 모두 그 값을 바꿔야 할 것이다.
해결방법
비가상 인터페이스 관용구를 사용하는 것이 있다.
파생 클래스에서 재정의 할 수 있는 가상 함수를 private 멤버로 두고, 이 가상 함수를 호출하는 public 비 가상 함수를 기본 클래스에 만들어 두는 것이다.
class Shape
{
public:
enum ShapeColor( Red, Green, Blue );
void draw(ShapeColor color = Red) const // 비가상함수
{
doDraw(color); // 가상함수 호출
}
...
private:
virtual void doDraw(ShapeColor color) const = 0; // 진짜 작업
};
class Rectangle: public Shape
{
private:
virtual void doDraw(ShapeColor color) const; // 기본 매개변수 값이 없다.
...
};
비가상 함수는 파생 클래스에서 오버라이드 되면 안 되기 때문에(항목 36 참조), 위와 같이 설계하면 draw 함수의 color 매개변수에 대한 기본값을 깔끔하게 Red로 고정시킬 수 있다.
요약
- 상속받은 기본 매개변수 값은 절대로 재정의해서는 안된다. 왜냐하면 기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수는 동적으로 바인딩되기 때문이다.
'Books > Effective C++' 카테고리의 다른 글
댓글
이 글 공유하기
다른 글
-
[Effective C++] 39. private 상속은 심사숙고해서 구사하자
[Effective C++] 39. private 상속은 심사숙고해서 구사하자
2022.07.03 -
[Effective C++] 38. "has-a(..는..를 가짐)" 혹은 "is-implemented-in-terms-of(..는..를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자
[Effective C++] 38. "has-a(..는..를 가짐)" 혹은 "is-implemented-in-terms-of(..는..를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자
2022.07.03 -
[Effective C++] 36. 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!
[Effective C++] 36. 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!
2022.07.02 -
[Effective C++] 35. 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자
[Effective C++] 35. 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자
2022.06.26