본문 바로가기

컴퓨터과학

[Effective C++] (12-2) 재정의 함수들을 override로 선언할 것

C++ override

* C++ override 관련 Chapter(12-1)에서 이어지는 내용

 

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();
virtual void mf2(unsigned int x);
cirtual void mf3() &&;
void mf4() const;
};

 

답은 다음과 같습니다.

 

- Base에서는 mf1이 const로 선언되었지만, Derived에서는 그렇지 않습니다.
- Base에서는 mf2가 int를 받지만, Derived에서는 unsigned int를 받습니다.
- Base에서는 mf3이 왼 값으로 한정되지만, Derived에서는 오른 값으로 한정됩니다.
- Base에서 mf4는 virtual로 선언되지 않았습니다.

 

아마 "실제 응용에서는 이런 문제들을 컴파일러가 경고를 해주므로 내가 신경 쓸 필요는 없지 않을까?"라고 생각하는 독자도 있을 것입니다. 그럴 수도 있지만, 아닐 수도 있습니다. 내가 점검해 본 두 컴파일러는 모든 경고를 활성화했는데도 위의 코드를 아무 불평 없이 컴파일했습니다. (다른 컴파일러들은 몇몇 문제점을 경고해주기도 했지만, 모든 문제점을 지적하지는 않았습니다.)
파생 클래스 재정의 선언은 제대로 해내야 하는 중요한 사항이지만, 프로그래머가 실수하기도 쉽습니다. 그래서 C++11은 파생 클래스 함수가 기반 클래스의 버전을 재정의하려 한다는 의도를 명시적으로 표현하는 방법을 제공합니다. 바로, 그런 파생 클래스 함수를 override로 선언하는 것입니다. 다음은 앞에 나온 선언들에 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;
};

 

물론 이 코드는 컴파일되지 않습니다. 컴파일되지 않도록 작성되었기 때문입니다. 컴파일러는 재정의 관련 문제점들을 모두 지적해줍니다. 이것이 우리가 원했던 결과이고, 모든 재정의 함수를 override로 선언해야 할 이유이기도 합니다.
다음은 override를 사용하는, 그리고 제대로 컴파일되는 코드입니다(Derived의 모든 함수가 Base의 가상 함수들을 재정의하는 것이 목표라고 할 때).

 

class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
virtual void mf4() const;
};

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을 붙여도 되지만, 꼭 그럴 필요는 없다
};

 

이 예에서, Base의 mf4를 가상 함수로 선언하는 것도 코드가 제대로 컴파일되게 하는 데 필요한 일임을 주목하기 바랍니다. 재정의 관련 오류는 대부분 파생 클래스에서 일어나지만, 기반 클래스에서 뭔가 잘못되었을 가능성도 있습니다.
파생 클래스의 모든 재정의 함수를 override로 선언한다는 방침은, 재정의를 의도한 함수가 살제로는 아무것도 재정의하지 않는다는 점을 컴파일러가 지적해 주는 것 이상의 장점을 제공합니다. 그러한 방침은 기반 클래스의 한 가상 함수의 서명을 변경했을 때 그 영향이 어느 정도인지 가늠하려 할 떄에도 도움이 됩니다. 파생 클래스들에서 override를 일관되게 적용했다면, 그냥 기반 함수의 서명을 변경하고, 시스템을 다시 컴파일하고, 피해가 어느 정도인지(즉, 컴파일에 실패한 파생 클래스가 몇 개나 되는지) 파악하고, 그에 따라 해당 서명을 그렇게 변경하는 것이 가치가 있는 일인지 판단하면 됩니다. 그러나 override를 사용하지 않았다면, 상세한 단위 검사(unit test)들을 마련해 두길 잘했다는 생각이 들(또는 그렇지 않았음을 후회할) 것입니다. 왜냐하면, 앞에서 보았듯이 기반 클래스의 함수들을 재정의하려 했지만 실제로는 그렇지 않은 파생 클래스 가상 함수들에 대해 컴파일러가 아무런 경고도 하지 않기 때문입니다.
예전에도 C++에는 많은 키워드가 있었지만, C++11은 거기에 두 개의 문맥 의존 키워드(contextual keyword) override와 final을 추가했습니다. 이 키워드들은 오직 특정한 문맥에서만 예약어로 작용한다는 특성을 가지고 있습니다. override는 멤버 함수 선언의 끝에 나올 때에만 예약된 의미를 가집니다. 따라서, override라는 이름을 사용 하는 구식 코드가 남아 있다고 해도, C++11을 위해 그 이름을 변경할 필요는 없습니다.