서적 정리/Effective C++

37.어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자

민돌이2 2021. 12. 22. 01:17

가상 함수는 동적 바인딩(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같이 자료형 선언을 안해도 알아서 자료형을 결정하는 것도 포함)

장점 : 런타임까지 타입에 대한 결정을 끌고 갈 수 있기 때문에 선택의 여지가 많다.

단점 : 실행 도중 예상치 못한 타입이 들어와 에러가 발생 증가

728x90