[Effective Modern C++] 12. 재정의 함수들은 override로 선언하자
재정의가 일어나려면 다음과 같은 여러 필수조건을 만족해야 한다.
- 기반 클래스 함수가 반드시 가상 함수이어야 한다.
- 기반 함수와 파생 함수의 이름이 반드시 동일해야 한다(단, 소멸자는 제외).
- 기반 함수와 파생 함수의 매개변수 타입들이 반드시 동일해야 한다.
- 기반 함수와 파생 함수의 const성이 반드시 동일해야 한다.
- 기반 함수와 파생 함수의 반환 타입과 예외 지정(exception specification)이 반드시 호환되어야 한다.
- [C++11에서 추가된 조건] 멤버 함수들의 참조 한정사(reference qualifier)들이 반드시 동일해야 한다.
이러한 모든 재정의 요구조건들이 뜻하는 것은, 작은 실수가 큰 차이를 빚을 수 있다는 것이다.
재정의 실수가 포함된 코드는 프로그래머가 의도한 것과는 다르게 행동하기 마련이지만, 컴파일 오류가 아닌 경우가 많다.
그래서 C++11은 파생 클래스 함수가 기반 클래스의 버전을 재정의하려 한다는 의도를 명시적으로 표현하는 방법을 제공한다. 바로, 파생 클래스 함수를 override로 선언하는 것이다.
파생 함수를 override로 선언하면, 컴파일러는 재정의 관련 문제점들을 모두 지적해준다.
class Base
{
public:
virtual void mf1(void) const;
virtual void mf2(int x);
virtual void mf3(void) &;
void mf4(void) const;
};
class Derived: public Base
{
public:
virtual void mf1(void);
virtual void mf2(unsigned int x);
virtual void mf3(void) &&;
void mf4(void) const;
};
// 다음은 위의 코드에서 발견된 네 가지 재정의 실수들이다.
// 1. Base에서는 mf1이 const로 선언되었지만, Derived에서는 그렇지 않다.
// 2. Base에서는 mf2가 int를 받지만, Derived에서는 unsigned int를 받는다.
// 3. Base에서는 mf3이 좌측값으로 한정되지만, Derived에서는 우측값으로 한정된다.
// 4. Base에서 mf4는 virtual로 선언되지 않았다.
// 이런 실수들이 있음에도 불구하고, 문법적으로 문제가 없기 때문에 컴파일러는 불만 없이 이 코드들을 컴파일 하는 경우가 많다.
// 이런 경우 재정의가 일어나지 않고 Base 클래스의 멤버 함수들이 가려질 뿐이다.
// 컴파일러로부터 이러한 재정의 실수들을 지적받고 싶다면 재정의 함수들을 override로 선언해야 한다.
class Derived: public Base
{
public:
virtual void mf1(void) override;
virtual void mf2(unsigned int x) override;
virtual void mf3(void) && override;
virtual void mf4(void) const override;
};
// 이제 이 코드는 컴파일되지 않는다.
// 다음은 override를 사용하는, 그리고 제대로 컴파일되는 코드이다.
class Base
{
public:
virtual void mf1(void) const;
virtual void mf2(int x);
virtual void mf3(void) &;
virtual void mf4(void) const;
};
class Derived: public Base
{
public:
virtual void mf1(void) const override;
virtual void mf2(int x) override;
virtual void mf3(void) & override;
void mf4(void) const override; // virtual은 선택적으로 붙이면 된다.
};
// Base의 mf4를 가상 함수로 선언하는 것도 코드가 제대로 컴파일되게 하는 데 필요한 일임을 기억하자.
// 재정의 관련 오류는 대부분 파생 클래스에서 일어나지만, 기반 클래스에서 뭔가 잘못되었을 가능성도 있다.
파생 클래스를 override로 선언한다는 방침은, 재정의를 의도한 함수가 실제로는 아무것도 재정의하지 않는다는 점을 컴파일러가 지적해 주는 것 이상의 장점을 제공한다.
그러한 방침은 기반 클래스의 한 가상 함수의 시그니처를 변경했을 때 그 영향이 어느 정도인지 가늠하려 할 때에도 도움이 된다.
참고로 C++11에는 두 개의 문맥 의존 키워드(contextual keyword) override, final가 추가되었다. 이 키워드들은 오직 특정한 문맥에서만 예약어로 작용하는 특성이 있어서, override나 final이라는 이름을 사용하는 구식 코드가 남아 있다고 해도, C++11을 위해 그 이름을 변경할 필요는 없다.
그리고 멤버 함수 참조 한정사를 사용하면 멤버 함수를 좌측값 혹은 우측값에만 사용 할 수 있게 제한 할 수 있으며 이 제한은 가상함수가 아닌 멤버 함수에도 적용 할 수 있다.
class Widget {
public:
...
void doWork(void) &; // doWork의 이 버전은 *this가 좌측값일 때에만 적용된다.
void doWork(void) &&; // doWork의 이 버전은 *this가 우측값일 때에만 적용된다.
};
...
Widget makeWidget(void); // 팩터리 함수(우측값을 돌려줌)
Widget w; // 보통 객체(좌측값을 돌려줌)
...
w.doWork(); // 좌측값용 Widget::doWork(즉, Widget::doWork &)를 호출한다.
makeWidget().doWork(); // 우측값용 Widget::doWork(즉, Widget::doWork &&)를 호출한다.
//---
// 좌측값 인수만 받는 함수를 작성하고 싶다면, 비 const 좌측값 참조 매개변수를 선언하면 된다.
void doSomething(Widget& w); // 좌측값 Widget만 받는 함수
// 우측값 인수만 받는 함수를 작성하고 싶다면, 우측값 참조 매개변수를 선언하면 된다.
void doSomething(Widget&& w); // 우측값 Widget만 받는 함수
// 멤버 함수 참조 한정사는 멤버 함수가 호출되는 객체, 즉 *this에 대해 이러한 구분이 가능하게 만드는 것일 뿐이다.
// 멤버 함수 참조 한정사는 주어진 멤버 함수가 호출되는 대상(*this)이 const임을 명시하기 위해 멤버 함수 선언 끝에 붙이는 const와 딱 비슷하다.
//---
// 이런 문법이 필요한 예.
// Widget 클래스에 std::vector 데이터 멤버가 있으며, 그것에 직접 접근할 수 있는 접근용 멤버 함수를 클라이언트에게 제공한다고 하자.
class Widget
{
public:
using DataType = std::vector<double>;
...
DataType& data(void) { return values; }
...
private:
DataType values;
};
// 다음과 같은 클라이언트 코드에서 어떤 일이 일어나는지 생각해 보자.
Widget w;
...
auto vals1 = w.data(); // w.values를 vals1에 복사
// Widget::data의 반환 타입은 좌측값 참조이고 좌측값 참조는 정의상 좌측값으로 취급되므로, 이 코드는 하나의 좌측값으로 vals1을 초기화한다.
// 따라서, vals1은 w.values 로부터 복사 생성된다.
// 다음으로, Widget을 생성하는 팩터리 함수가 있다고 하자.
Widget makeWidget(void);
// 그리고 이 makeWidget이 돌려준 Widget 객체 안의 std::vector를 이용해서 변수를 초기화한다고 하자.
auto vals2 = makeWidget(void).data(); // Widget 안에 있는 values를 vals2에 복사
// 이번에도 Widget::data는 좌측값 참조를 돌려주며, 마찬가지로 vals2는 Widget안의 values로부터 복사 생성된다.
// 그러나 이번에는 Widget이 makeWidget이 돌려준 임시 객체(즉, 우측값)이다.
// 따라서 그 임시 객체 안의 std::vector를 복사하는 것은 시간 낭비이다.
// 이를 해소하기 위해 정말로 필요한 것은 data가 우측값 Widget에 대해 호출된 경우에는 반드시 우측값을 돌려주게 하는 것이다.
// 해결법: data를 좌측값 Widget과 우측값 Widget에 대해 개별적으로 오버로딩.
class Widget
{
public:
using DataType = std::vector<double>;
...
DataType& data(void) & { return values; } // 좌측값 Widget에 대해서는 좌측값을 반환
DataType&& data(void) && { return std::move(values); } // 우측값 Widget에 대해서는 우측값을 반환
...
private:
DataType values;
};
auto vals1 = w.data(); // Widget::data의 좌측값 오버로딩된 함수를 호출; vals1은 복사 생성됨
auto vals2 = makeWidget().data(); // Widget::data의 우측값 오버로딩된 함수를 호출; vals2는 이동 생성됨
'Books > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 14. 예외를 방출하지 않을 함수는 noexcept로 선언하자 (0) | 2022.09.04 |
---|---|
[Effective Modern C++] 13. iterator보다 const_iterator를 선호하자 (0) | 2022.09.04 |
[Effective Modern C++] 11. 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하자 (0) | 2022.09.03 |
[Effective Modern C++] 10. 범위 없는 enum보다 범위 있는 enum을 선호하자 (0) | 2022.08.28 |
[Effective Modern C++] 9. typedef보다 별칭 선언을 선호하자 (0) | 2022.08.28 |
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 14. 예외를 방출하지 않을 함수는 noexcept로 선언하자
[Effective Modern C++] 14. 예외를 방출하지 않을 함수는 noexcept로 선언하자
2022.09.04 -
[Effective Modern C++] 13. iterator보다 const_iterator를 선호하자
[Effective Modern C++] 13. iterator보다 const_iterator를 선호하자
2022.09.04 -
[Effective Modern C++] 11. 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하자
[Effective Modern C++] 11. 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하자
2022.09.03 -
[Effective Modern C++] 10. 범위 없는 enum보다 범위 있는 enum을 선호하자
[Effective Modern C++] 10. 범위 없는 enum보다 범위 있는 enum을 선호하자
2022.08.28