37.어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
가상 함수는 동적 바인딩(dynamically binding)이고 비가상 함수는 정적 바인딩(static binding)이다.
기본 매개변수 값을 가진 가상 함수를 상속하는 경우 가상 함수는 동적 바인딩하지만, 기본 매개변수 값은 정적 바인딩을 한다.
class Shape
{
public:
enum ShapeColor { Red, Green, Blue };
virtual void Draw(ShapeColor color = ShapeColor::Red) const = 0;
};
class Rectangle : public Shape
{
virtual void Draw(ShapeColor color = ShapeColor::Green) const override; //기본 매개변수 있음
};
class Circle : public Shape
{
virtual void Draw(ShapeColor color) const override; //기본 매개변수 없음
};
int main()
{
Shape* ps; //정적 타입 = Shape*
Shape* pc = new Circle; //정적 타입 = Shape*
Shape* pr = new Rectangle; //정적 타입 = Shape*
system("Pause");
return 0;
}
객체의 정적 타입(static type)(*주 1)은 프로그램 소스 안에 놓는 선언문을 통해 그 객체가 갖는 타입이다.
ps, pc 및 pr은 모두 Shape에 대한 포인터로 선언되어 있기 때문에, 각각의 정적 타입도 모두 Shape 포인터 타입이다.
객체의 동적 타입(dynamic type)(*주 1)은 현재 그 객체가 진짜로 무엇이냐에 따라 결정되는 타입이다. 즉, 이 객체가 어떻게 동작할 것이냐를 가리킨느 타입을 동적 타입이라고 한다. pc의 동적 타입은 Circle*이고 pr의 동적 타입은 Rectangle*이다. ps의 동적 타입은 없다. 아무 개체도 참조하고 있지 않기 때문이다.
동적 타입은 프로그램이 실행되는 도중에 바뀔 수 있다. 대개 대입문을 통해 바꾼다.
ps = pc; //이제 ps의 동적 타입은 Circle*이 된다.
ps = pr; //이제 ps의 동적 타입은 Rectangle*이 된다.
가상 함수는 동적으로 바인딩된다. 가상 함수의 호출이 일어난 객체의 동적 타입에 따라 어떤 가상 함수가 호출될지가 결정된다는 의미이다. 말이 어려워 예시로 보여주자면
int main()
{
Shape* ps; //정적 타입 = Shape*
Shape* pc = new Circle; //정적 타입 = Shape*
Shape* pr = new Rectangle; //정적 타입 = Shape*
ps = pc; //이제 ps의 동적 타입은 Circle*이 된다.
ps->Draw(); //Circle::Draw() 호출
ps = pr; //이제 ps의 동적 타입은 Rectangle*이 된다.
ps->Draw(); //Rectangle::Draw() 호출
delete pc;
delete pr;
system("Pause");
return 0;
}
위에서 가상 함수는 동적 바인딩이지만, 기본 매개변수는 정적 바인딩이라고 했었다.
이 말은 즉슨, 파생 클래스의 객체에서 기본 매개변수를 사용한다면 기본 클래스의 기본 매개변수를 사용한다는 것이다.
pr->Draw(); //Rectangle::Draw(ShapeColor::Red)를 호출함
pr의 동적 타입이 Rectangle*이므로, 호출되는 가상 함수는 Rectangle의 Draw 함수가 호출될 것이다. 그리고 Rectangle::Draw 함수에서 기본 매개변수 값이 Green으로 되어있다. 하지만 pr의 정적 타입은 Shape*이기 때문에, Draw 함수에서 쓰이는 기본 매개변수 값을 Shape 클래스에서 가져온다.
만약 함수의 기본 매개변수가 동적으로 바인딩된다면, 프로그램 실행 중에 가상 함수의 기본 매개변수 값을 결정할 방법을 컴파일러 쪽에서 마련해 주어야 한다. 이 방법은 컴파일 과정에서 결정하는 현재의 매커니즘보다 느리고 복잡하기 때문에 런타임 효율이 나쁘기 때문에 기본 매개변수는 정적으로 바인딩한다.
비가상 인터페이스(non-virtual interface : NVI) 관용구를 사용하면 함수의 기본 매개변수에 대한 값을 고정시킬 수 있다.
class Shape
{
public:
enum ShapeColor { Red, Green, Blue };
void Draw(ShapeColor color = ShapeColor::Red) const //비가상 함수이기 때문에 재정의하면 안됨
{
DrawHelper(color);
}
private:
virtual void DrawHelper(ShapeColor color) const = 0;
};
class Rectangle : public Shape
{
virtual void DrawHelper(ShapeColor color) const override;
};
class Circle : public Shape
{
virtual void DrawHelper(ShapeColor color) const override;
};
비가상 함수는 파생 클래스에서 오버라이드하면 안 되기 때문에 NVI 관용구로 설계하면 기본 매개변수 값을 깔끔하게 사용할 수 있다.
이것만은 잊지 말자
상속받은 기본 매개변수 값은 절대로 재정의해서는 안된다. 기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수는 동적으로 바인딩되기 때문이다.
(*주 1) 정적 타입 vs 동적 타입
정적 타입 : 자료형을 컴파일 시에 결정하는 것
장점 : 컴파일 시에 타입에 대한 정보를 결정하기에 속도 증가, 에러를 링커로 확인이 가능하여 안전성 증가
동적 타입 : 자료형을 런타임 중 결정하는 것(Python같이 자료형 선언을 안해도 알아서 자료형을 결정하는 것도 포함)
장점 : 런타임까지 타입에 대한 결정을 끌고 갈 수 있기 때문에 선택의 여지가 많다.
단점 : 실행 도중 예상치 못한 타입이 들어와 에러가 발생 증가