[C++] 항목 37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 6. 상속, 그리고 객체 지향 설계
👉🏻 항목 37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
✅ 문제 상황: 기본 매개변수를 재정의하면?
다음은 도형을 그리는 클래스 계층 구조이다:
// 기하학 도형을 나타내는 클래스
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;
...
};
이제 이 클래스들을 사용해보자:
int main() {
// 정적 타입 = Shape*, 동적 타입 = Null
Shape* ps;
// 정적 타입 = Shape*, 동적 타입 = Circle*
Shape* pc = new Circle;
// 정적 타입 = Shape*, 동적 타입 = Rectangle*
Shape* pr = new Rectangle;
ps = pc; // ps의 동적 타입 = Circle*
ps = pr; // ps의 동적 타입 = Rectangle*
pc->draw(Shape::Red); // Circle::draw(Shape::Red) 호출
pr->draw(Shape::Red); // Rectangle::draw(Shape::Red) 호출
// 문제 발생!
pr->draw(); // Rectangle::draw(Shape::Red) 호출
}
✅ 무엇이 문제인가?
pr->draw()를 호출했을 때:
- 호출되는 가상 함수는
Rectangle의draw함수이다. (동적 바인딩) - 하지만, 정적 타입이
Shape*이므로, 기본 매개변수는Shape클래스에서 가져온다! - 결과:
Rectangle::draw(Shape::Red)가 호출된다.
예상: Rectangle::draw(Shape::Green) 호출
실제: Rectangle::draw(Shape::Red) 호출
이상한 조합이 발생한다:
- 함수 본체는
Rectangle의 것 (동적 바인딩) - 기본 매개변수는
Shape의 것 (정적 바인딩)
✅ 왜 이런 일이 발생하는가?
핵심 원인: 바인딩 방식의 차이
- 가상 함수는 동적 바인딩(dynamic binding) 된다.
- 런타임에 실제 객체의 타입에 따라 호출될 함수가 결정된다.
- 기본 매개변수 값은 정적 바인딩(static binding) 된다.
- 컴파일 시점에 포인터/참조의 선언된 타입에 따라 결정된다.
이유:
- 동적 바인딩은 런타임 오버헤드가 있다.
- 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;
...
};
기본 클래스와 파생 클래스에서 동일한 기본 매개변수를 사용하면,
→ 예상 외의 동작은 일어나지 않는다.
하지만 이 방법의 문제점:
- 코드 중복
- 모든 파생 클래스에서 같은 기본값을 반복해야 한다.
- 의존성 증가
Shape클래스의 기본 매개변수 값이 바뀌면, 모든 파생 클래스의 매개변수 값도 바꿔줘야 한다.- 유지보수가 어렵다.
✅ 해결 방법: 비가상 인터페이스(NVI) 관용구 사용
항목 35에서 배운 NVI 관용구를 활용하자:
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 {
public:
...
private:
// 기본 매개변수 값이 없다
virtual void doDraw(ShapeColor color) const;
};
이 방법의 장점:
draw함수는 비가상 함수이므로 파생 클래스에서 재정의할 수 없다. (항목 36)- 따라서
draw함수의color기본 매개변수를Red로 안전하게 고정시킬 수 있다. - 실제 그리기 동작은
doDraw에서 구현하고, 기본 매개변수는draw에서 관리한다. - 코드 중복도 없고, 의존성 문제도 해결된다.
동작 방식:
Shape* pr = new Rectangle;
pr->draw(); // Shape::draw(Red) 호출 → Rectangle::doDraw(Red) 호출
draw()는 비가상이므로 항상Shape::draw가 호출된다.- 기본 매개변수
Red가 확실히 전달된다. doDraw(Red)는 가상 함수이므로Rectangle::doDraw(Red)가 호출된다.
※ 항목 35의 function을 사용하여도 해결할 수 있다.
✅ 정리하면
왜 기본 매개변수를 재정의하면 안 되는가?
| 항목 | 바인딩 방식 | 결정 시점 | | — | — | — | | 가상 함수 | 동적 바인딩 | 런타임 (실제 객체 타입) | | 기본 매개변수 | 정적 바인딩 | 컴파일 타임 (포인터/참조 타입) |
- 두 가지가 서로 다른 시점에 결정되므로, 예상치 못한 조합이 발생한다.
- 함수는 파생 클래스의 것이 호출되지만, 기본 매개변수는 기본 클래스의 것이 사용된다.
🧐 정리
- 상속받은 기본 매개변수 값은 절대로 재정의하지 말자.
- 기본 매개변수는 정적 바인딩, 가상 함수는 동적 바인딩되기 때문이다.
- 모든 파생 클래스에서 동일한 기본 매개변수를 사용하는 방법도 있지만, 코드 중복과 의존성 문제가 있다.
- NVI 관용구를 사용하면 이 문제를 깔끔하게 해결할 수 있다.
- 비가상 함수에서 기본 매개변수를 관리하고, 실제 동작은 private 가상 함수에서 구현한다.
- function 함수를 사용하여도 해결할 수 있다.
댓글남기기