[C++] 항목 37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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()를 호출했을 때:

  • 호출되는 가상 함수는 Rectangledraw 함수이다. (동적 바인딩)
  • 하지만, 정적 타입이 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;
    ...
};

기본 클래스와 파생 클래스에서 동일한 기본 매개변수를 사용하면,
→ 예상 외의 동작은 일어나지 않는다.

하지만 이 방법의 문제점:

  1. 코드 중복
    • 모든 파생 클래스에서 같은 기본값을 반복해야 한다.
  2. 의존성 증가
    • 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을 사용하여도 해결할 수 있다.


✅ 정리하면

왜 기본 매개변수를 재정의하면 안 되는가?

| 항목 | 바인딩 방식 | 결정 시점 | | — | — | — | | 가상 함수 | 동적 바인딩 | 런타임 (실제 객체 타입) | | 기본 매개변수 | 정적 바인딩 | 컴파일 타임 (포인터/참조 타입) |

  • 두 가지가 서로 다른 시점에 결정되므로, 예상치 못한 조합이 발생한다.
  • 함수는 파생 클래스의 것이 호출되지만, 기본 매개변수는 기본 클래스의 것이 사용된다.

🧐 정리

  1. 상속받은 기본 매개변수 값은 절대로 재정의하지 말자.
    • 기본 매개변수정적 바인딩, 가상 함수동적 바인딩되기 때문이다.
  2. 모든 파생 클래스에서 동일한 기본 매개변수를 사용하는 방법도 있지만, 코드 중복과 의존성 문제가 있다.
  3. NVI 관용구를 사용하면 이 문제를 깔끔하게 해결할 수 있다.
    • 비가상 함수에서 기본 매개변수를 관리하고, 실제 동작은 private 가상 함수에서 구현한다.
  4. function 함수를 사용하여도 해결할 수 있다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기