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 함수를 사용하여 적절한 상황에 맞게 호출함으로 불필요한 복사를 방지할 수 있다.
'컴퓨터 > Modern Effective C++ 정리' 카테고리의 다른 글
Modern Effective C++ item 19 : 공유자원은 std::shared_ptr (0) | 2015.04.14 |
---|---|
Modern Effective C++ Item 17 : 자동 생성 함수 in C++11 (1) | 2015.04.08 |
Modern Effective C++ Item 11 : Private 봉인술 <<< Delete 봉인술 (2) | 2015.03.24 |
Modern Effective C++ Item 10 : enum <<< enum class (0) | 2015.03.24 |
Modern Effective C++ Item 6 : auto로 안되면 명타초 쓰자 (0) | 2015.03.10 |