[Modern C++] 항목 12: 재정의 함수들을 override로 선언하라
카테고리: Cpp
이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역
가독성이 떨어지는 직역들을 수정하며 정리하였습니다. e.g. 연역 → 추론, 중복적재 → 오버로딩
📦 3. 현대적 C++에 적응하기
👉🏻 항목 12: 재정의 함수들을 override로 선언하라
🔍 가상 함수 오버라이딩 개요
- 가상 함수 오버라이딩(override): 자식 클래스 함수를 부모 클래스의 인터페이스를 통해 호출할 수 있게 만드는 메커니즘
- 오버라이딩(overriding)과 오버로딩(overloading)은 완전히 별개의 개념이다.
class Base {
public:
virtual void doWork(); // 부모 클래스 가상 함수
...
};
class Derived : public Base {
public:
virtual void doWork(); // Base::doWork를 오버라이딩한다
... // (여기서는 virtual을 생략할 수 있다)
};
// 자식 클래스 객체를 가리키는 부모 클래스 포인터를 생성한다
std::unique_ptr<Base> upb = std::make_unique<Derived>();
// 부모 클래스 포인터로 doWork를 호출하며,
// 자식 클래스의 함수가 호출된다.
upb->doWork();
✅ 오버라이딩 필수 조건
오버라이딩이 일어나려면 다음 조건들을 모두 만족해야 한다.
- 부모 클래스 함수가 반드시 가상 함수이어야 한다.
- 부모 함수와 자식 함수의 이름이 반드시 동일해야 한다 (단, 소멸자는 예외).
- 부모 함수와 자식 함수의 매개변수 타입들이 반드시 동일해야 한다.
- 부모 함수와 자식 함수의
const성이 반드시 동일해야 한다. - 부모 함수와 자식 함수의 반환 타입과 예외 명세가 반드시 일치해야 한다.
- (C++11 추가) 부모 함수와 자식 함수의 참조 한정자(reference qualifier)들이 반드시 동일해야 한다.
⚠️ 오버라이딩 실수 예시
- 작은 실수가 큰 차이를 낳을 수 있다. 오버라이딩 실수가 포함된 코드는 컴파일 오류 없이 다르게 행동한다.
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const;
};
class Derived : public Base {
public:
virtual void mf1(); // ❌ Base: const, Derived: 아님
virtual void mf2(unsigned int x); // ❌ 매개변수 타입 다름
virtual void mf3() &&; // ❌ Base: lvalue 한정, Derived: rvalue 한정
void mf4() const; // ❌ Base에서 virtual이 아님
};
- 위 코드는 컴파일은 되지만, 자식 클래스 함수 중 부모 클래스 함수를 오버라이딩하는 것은 하나도 없다.
🏷️ override 키워드
- C++11은 오버라이딩 의도를 명시적으로 표현하는 방법을 제공한다. → 자식 클래스 함수를
override로 선언한다.
class Derived : public Base {
public:
virtual void mf1() override;
virtual void mf2(unsigned int x) override;
virtual void mf3() && override;
virtual void mf4() const override;
};
- 위 코드는 컴파일되지 않는다. 컴파일러가 오버라이딩 관련 문제점들을 모두 지적해준다.
- 이것이 우리가 원하는 결과다.
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
virtual void mf4() const; // virtual 추가
};
class Derived : public Base {
public:
virtual void mf1() const override;
virtual void mf2(int x) override;
virtual void mf3() & override;
void mf4() const override; // virtual을 붙여도 되지만, 꼭 그럴 필요는 없다
};
override의 추가 장점: 부모 클래스의 가상 함수 시그니처를 변경했을 때 영향 범위를 즉시 파악할 수 있다.override를 일관되게 적용했다면, 부모 함수의 시그니처를 변경하고 다시 컴파일하면 컴파일에 실패한 자식 클래스의 수로 피해를 가늠할 수 있다.
override와final은 문맥 의존 키워드(contextual keyword)다.
멤버 함수 선언의 끝에 나올 때에만 예약된 의미를 가진다.
따라서override라는 이름을 사용하는 구식 코드가 남아 있어도, C++11을 위해 이름을 변경할 필요는 없다.
📌 멤버 함수 참조 한정자
- 참조 한정자(reference qualifier): 멤버 함수가 호출되는 객체(
this)를lvalue또는rvalue로 한정하는 기능이다.
class Widget {
public:
...
void doWork() &; // *this가 lvalue일 때에만 적용
void doWork() &&; // *this가 rvalue일 때에만 적용
};
Widget makeWidget(); // 팩터리 함수 (rvalue를 돌려줌)
Widget w; // 보통 객체 (lvalue)
// lvalue용 Widget::doWork
// (즉, Widget::doWork &)를 호출
w.doWork();
// rvalue용 Widget::doWork
// (즉, Widget::doWork &&)를 호출
makeWidget().doWork();
- 참조 한정자의 활용 예시:
Widget클래스에std::vector멤버 변수가 있고, 이에 직접 접근하는data()함수를 제공한다고 하자.
class Widget {
public:
using DataType = std::vector<double>;
...
DataType& data() & // lvalue Widget에 대해서는 lvalue를 반환
{ return values; }
DataType&& data() && // rvalue Widget에 대해서는 rvalue를 반환
{ return std::move(values); }
...
private:
DataType values;
};
// Widget::data의 lvalue 오버로드를 호출
// → vals1은 복사 생성됨
auto vals1 = w.data();
// Widget::data의 rvalue 오버로드를 호출
// → vals2는 이동 생성됨
auto vals2 = makeWidget().data();
- 한 멤버 함수에 참조 한정자가 붙어 있으면, 그 함수의 모든 오버로드에도 참조 한정자를 지정해야 한다.
- 참조 한정되지 않은 오버로드는
lvalue/rvalue객체 모두에 대해 호출될 수 있어, 참조 한정된 오버로드들과 경쟁하며 모든 호출이 모호해진다.
- 참조 한정되지 않은 오버로드는
🧐 정리
- 오버라이딩 함수는 반드시
override로 선언하라. 컴파일러가 오버라이딩 관련 실수를 컴파일 시점에 즉시 잡아준다. - 멤버 함수 참조 한정자를 이용하면 멤버 함수가 호출되는 객체(
this)의lvalue버전과rvalue버전을 다른 방식으로 처리할 수 있다.
댓글남기기