37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
일단 이번 문제의 원인은 다음과 같다. 가상 함수는 동적으로 바인딩되지만, 기본 매개변수 값은 정적으로 바인딩된다는 것이다.
객체의 정적 타입(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 Rectanble; // 정적 타입 = Shape*
여기서 ps, pc 및 pr 모두 'Shape에 대한 포인터'로 선언되어 있기 때문에, 정적 타입도 모두 이 타입이다.
객체의 동적 타입(dynamic type)은 현재 그 객체가 진짜로 무엇이냐에 따라 결정되는 타입이다. 다시 말해, '이 객체가 어떻게 동작할 것이냐'를 가리키는 타입이 동적 타입이라 하겠다.
동적 타입은 이럼처럼 프로그램이 실행되는 도중에 바뀔 수 있다.
ps = pc; // 동적 타입 = Circle*
ps = pr; // 동적 타입 = Rectangle*
가상 함수는 동적으로 바인딩 된다. 호출이 일어난 객체의 동적 타입에 따라 어떤 함수가 호출될지가 결정된다는 뜻이다.
pc->draw(Shape::Red); // Circle::draw(Shape::Red) 호출
pr->draw(Shape::Red); // Rectangle::draw(Shape::Red) 호출
그런데 '기본 매개변수 값이 설정된' 가상 함수로 오게 되면 꼬이기 시작한다. 이유는 앞에서 말했듯이, 가상 함수는 동적으로 바인딩되어 있지만 기본 매개변수는 정적으로 바인딩되어 있기 때문이다.
pr->draw(); // Rectangle::draw(Shape::Red)를 호출한다!
pr의 정적 타입은 Shape*이기 때문에, 지금 호출되는 가상 함수에 쓰이는 기본 매개변수 값을 Shape 클래스에서 가져온다. 참으로 기상천외한 함수 호출이 이루어진다! 이 문제는 참조자라도 동일하다.
어째서 C++는 이런 동작방식을 고집하는 걸까? 여기에는 런타임 효율이라는 요소가 숨어 있다. 만약에 함수의 기본 매개변수가 동적으로 바인딩된다면, 프로그램 실행 중에 가상 함수의 기본 매개변수 값을 결정할 방법을 컴파일러 쪽에서 마련해 주어야 할 것이다. 이 방법은 현재의 메커니즘보다는 느리고 복잡할 것이 분명할 것이다.
클래스 및 파생 클래스의 사용자에게 기본 매개변수 값을 (똑같이) 제공해 보려고 하면 어떻게 되는지 확인해보자.
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;
};
위의 코드는 코드 중복에 의존성까지 걸려 있다. 기본 클래스의 기본 매개변수 값이 변하기라도 한다면 이 값을 반복하고 있는 파생 클래스는 모두 그 값으로 바꿔야 한다.
이 문제를 해결하려면 항목 35에서 가상 함수 대신에 사용하는 방법들을 사용하면 된다. 이들 중 하나인 NVI 패턴을 사용해보자. 비가상 함수가 기본 매개변수를 지정하도록 만들 수 있다. 이 비가상 함수의 내부에서는 진짜 일을 맡은 가상 함수를 호출하게 만든다.
class Shape {
public:
enum ShapeColor {Red, Green, Blue};
// 이제는 비가상 함수
void draw(ShapeColor color = Red) const
{
doDraw(color); // 가상 함수를 호출
}
private:
// 진짜 작업은 doDraw에서
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle : public Shape {
private:
// 기본 매개변수 값이 없다.
virtual void doDraw(ShapeColor color) const;
};
이제는 기본값을 깔끔하게 Red로 고정시킬 수 있다.
이것만은 잊지 말자!
- 상속받은 기본 매개변수 값은 절대로 재정의해서는 안 된다. 왜냐하면 기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수는 동적으로 바인딩되기 때문이다.