-
Notifications
You must be signed in to change notification settings - Fork 0
item 12 요약 정리
#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 없음
};
컴파일러가 바로 경고를 날려주니 걱정없다고 말하는 사람도 있을지 모른다. 하지만 필자가 체크해본 결과 컴파일러가 이런 문제를 제대로 체크하지 못했다고 한다. 실수를 막아주는 분명한 제약이 필요하다.
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 키워드가 어디에 놓여있느냐에 따라 달리 해석을 한다는 것이다. pre-occupied된 것이 아니다! 이것의 위대함은 이전에 override라는 이름의 함수를 사용하던 프로그래머가 직접적으로 느낄 수있다. override가 상황에 따라 해석되는 키워드가 아니라면 그가 짠 모든 코드를 전면적으로 수정해야 되기 때문이다. 우리는 이 키워드가 contextual keyword인 것에 안도의 한숨을 쉴 수 있다.
추가로 final이란 키워드도 contextual keyword의 하나로 소개되었다. class의 final속성을 선언하는데 사용되는 키워드이다. final 선언된 클래스는 다른 클래스의 부모 클래스가 될 수 없다. virtual 소멸자를 만들지 않은 라이브러리들을 무지한 사용자가 상속해서 쓰는 오류를 막아주는데 유용할 것이다.
위의 예제에서 잠시 소개된 이 개념은 잘 알려지지 않은 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는 이런 쓸데없는 작업을 하지않을 수 있을 것이다. 이 문제는 위에서 말했던 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 함수를 사용하여 적절한 상황에 맞게 호출함으로 불필요한 복사를 방지할 수 있다.