본문 바로가기
컴퓨터/Modern Effective C++ 정리

Modern Effective C++ Item 12: 오버라이딩 함수에 override를 선언하자.

by 김짱쌤 2015. 3. 24.

Item 12: 오버라이딩 함수에 override를 선언하자.

다형성의 핵심적인 기능중 하나는 부모클래스의 동작을 자식클래스가 오버라이딩 할 수 있다는 것이다.

class Base{
public:
   virtual void doWork(); //virtual로 선언한뒤...
...
};
class Derived: public Base{
public:
    virtual void doWork(); //오버라이딩!
...
};

std::unique_ptr<Base> upd = 
   std::make_unique<Derived>(); //unique_ptr에서 정적 포인터는 Base, 
                                //동적 포인터는 Derived로 만드는 방법
upd->doWork(); //동적 바인딩을 통해 Derived 객체의 doWork가 실행된다.

실수하기 쉬운 오버라이딩 규칙들

이러한 형태의 오버라이딩은 수많은 조건이 충족되어야만 성립된다.

  • 부모 클래스의 함수가 virtual일 것
  • 함수 이름이 반드시 같아야 할 것(소멸자 제외).
  • 함수 인자의 타입이 반드시 같아야 할 것.
  • 함수의 상수성이 일치해야 할 것.
  • 리턴 타입과 예외 지정이 호환가능해야 할 것.
  • 함수의 reference qualifier(차후 설명)가 동일할 것.(C++11부터 적용)

이 수 많은 조건을 항상 충족시키는 것은 쉽지 않은 일이다.

class Base{
public:
   virtual void mf1() const;
   virtual void mf2(int x);
   virtual void mf3() &;
   void mf4() const;
};
class Derived{
public:
   virtual void mf1();              //상수성이 다름
   virtual void mf2(unsigned int x);//인자 타입 다름
   virtual void mf3() &&;           //reference qualifier 다름
   void mf4() const;                //virtual 없음
};

컴파일러가 바로 경고를 날려주니 걱정없다고 말하는 사람도 있을지 모른다. 하지만 필자가 체크해본 결과 컴파일러가 이런 문제를 제대로 체크하지 못했다고 한다. 실수를 막아주는 분명한 제약이 필요하다.

C++ 11의 override선언으로 오버라이딩 제약을 걸 수 있다.

override선언은 오버라이딩한 해당 함수가 부모로부터 상속받을 수 있는 것인지 체크한다. 만약 체크에 실패하면 "재정의 지정자 'override'가 있는 메서드가 기본 클래스 메서드를 재정의하지 않았습니다."라는 컴파일 에러 메시지를 받을 수 있다. 상속받는 멤버함수임을 선언하여 제약을 거는 것으로 안전한 오버라이딩을 가능하게 한다.

class Derived:public Base{
   virtual void mf1() override;                  //컴파일 error
   virtual void mf2(unsigned int x) override;    //컴파일 error
   virtual void mf3() & override;                //오버라이딩 성공 코드
   void mf4() const override;                    //override선언하면 virtual도 겸한다. 성공 코드
};

override를 쓰면 오버라이딩한 부모의 함수의 signature가 변경된 경우 바로 오류(빨간줄)를 출력한다. 모든 자식클래스들의 파일에 빨간 밑줄이 쳐져있는 것을 보면서, 이 변경이 얼마나 큰 타격을 줄지 시각적으로 표현해주는 것이다. 만약 override선언이 없었다면, 모든 자식클래스들은 부모와 전혀 관계없는 함수를 virtual로 가지고 있는 바보같은 상황에 처할 것이다. 그리고 그 결과가 겉으로는 아무 문제를 일으키지 않는다면, 우리는 그 상황에 대해서 전혀 모른상태로 코딩을 계속할 것이다!!

override는 contextual keyword이다.

이 말인 즉슨 override 키워드가 어디에 놓여있느냐에 따라 달리 해석을 한다는 것이다. pre-occupied된 것이 아니다! 이것의 위대함은 이전에 override라는 이름의 함수를 사용하던 프로그래머가 직접적으로 느낄 수있다. override가 상황에 따라 해석되는 키워드가 아니라면 그가 짠 모든 코드를 전면적으로 수정해야 되기 때문이다. 우리는 이 키워드가 contextual keyword인 것에 안도의 한숨을 쉴 수 있다.

추가로 final이란 키워드도 contextual keyword의 하나로 소개되었다. class의 final속성을 선언하는데 사용되는 키워드이다. final 선언된 클래스는 다른 클래스의 부모 클래스가 될 수 없다. virtual 소멸자를 만들지 않은 라이브러리들을 무지한 사용자가 상속해서 쓰는 오류를 막아주는데 유용할 것이다.

reference qualifier에 대하여

위의 예제에서 잠시 소개된 이 개념은 잘 알려지지 않은 C++11의 새로운 기능의 하나이다. 이것은 상수 멤버 함수와 유사하다. 상수멤버함수는 멤버 함수 뒤에 const를 붙임으로 상수 타입 인스턴스가 호출할 수 있는 함수를 지정하는 것이다. reference qualifier는 &나 &&를 멤버함수 뒤에 붙여서 lvalue인 인스턴스와 rvalue인 인스턴스가 호출해야하는 함수를 직접 지정해주는 기능이다.

class Widget{
public:
   using DataType = std::vector<double>; //using을 통한 타입 별명짓기
   DataType& data(){return values;}      //data의 레퍼런스를 리턴하는 함수
private:
   DataType values;
};

Widget w;
auto vals1 = w.data();                  //w의 value로 복사생성자를 호출

이 경우는 필요에의해 w를 만들고 난뒤에 data를 복사하는 것이라 문제가 없다. 하지만 Widget을 만들어 리턴하는 펙토리 함수를 통해 만들어진 rvalue로 data()를 호출하는 경우

   auto vals2 = makeWidget().data();

기존의 멤버함수는 여전히 DataType&를 리턴한다. 따라서 vals2는 다시 복사생성자를 호출할 것이다.(rvalue를 받았는데도 불구하고!) 여기서 쓸데없는 복사가 발생한다. 만약 data()가 rvalue를 리턴한다면 vals2는 move생성자를 통해 이런 쓸데없는 작업을 하지않을 수 있을 것이다. 이 문제는 위에서 말했던 reference qualified function이 있으면 쉽게 해결할 수 있다.

class Widget{
public:
   using DataType = std::vector<double>;      //using을 통한 타입 별명짓기
   DataType& data() & 
   { return values; }            //*this가 lvalue인 경우는 원래랑 똑같이
   DataType data() && 
   { return std::move(values); } //rvalue라면 std::move로 rvalue 넘긴다.
private:
   DataType values;
};
...
Widget w;
auto vals1 = w.data();            //lvalue용 data()함수를 호출하여 lvalue전달
auto vals2 = makeWidget().data(); //rvalue용 data()함수를 호출하여 rvalue전달

위와 같이 Reference Qualified 함수를 사용하여 적절한 상황에 맞게 호출함으로 불필요한 복사를 방지할 수 있다.