* 두 개의 문맥 의존 키워드(contextual keyword) override와 final
이 키워드들은 오직 특정한 문맥에서만 예약어로 작용한다는 특성을 가지고 있습니다. override는 멤버 함수 선언의 끝에 나올 때에만 예약된 의미를 가집니다. 따라서, override라는 이름을 사용하는 구식 코드가 남아 있다고 해도, C++11을 위해 그 이름을 변경할 필요는 없습니다.
(* 가상 함수에 final을 적용하면 파생 클래스에서 그 함수를 재정의할 수 없게 됩니다. 클래스 자체에 final을 적용할 수도 있는데, 그러면 그 클래스는 기반 함수로 쓰일 수 없게 됩니다.
간단히 말해서 override와 final은 키워드가 아니라 식별자(identifier)입니다. contextual keyword는 적어도 C++11 표준 명세서에는 나오지 않는 용어로, 명세서들에서는 "identifiers with special meaning(특별한 의미를 가진 식별자)"이라고 부릅니다. 이들을 온전한 키워드로 만들지 않은 이유 중 하나는 본문에도 암시되어 있는 기존 코드와의 하위 호환성 문제입니다. 한편, C++11 표준 제정 과정에서는 이를테면
virtual void mf1 [[override]] () const;
처럼 '특성(attribute)'을 이용한 방법도 제안되었지만, 몇 가지 이유로 기각되고 결국은 지금의 방식이 채택되었습니다.)
void doSomething(Widget&& w); // 오른값 Widgget만 받는 함수
멤버 함수 참조 한정사는 멤버 함수가 호출되는 객체, 즉 *this에 대한 이러한 구분이 가능하게 만드는 것일 뿐입니다. 멤버 함수 참조 한정사는 주어진 멤버 함수가 호출되는 대상(즉, *this)이 const임을 명시하기 위해 멤버 함수 선언 끝에 붙이는 const와 딱 비슷합니다.
멤버 함수에 참조 한정사를 붙여야 하는 상황이 흔치는 않지만, 없는 것도 아닙니다. 예를 들어 Widget 클래스에 std::vector 자료 멤버가 있으며, 그것에 직접 접근할 수 있는 접근용 멤버 함수를 크라이언트에게 제공한다고 합시다.
clas Widget {
public:
using DataType = std::vector; // using에 관해서는 Chapter(9)를 보라
...
DataType& data() { return values; }
...
private:
DataType values;
};
이 클래스의 설계가 아주 잘 캡슐화되어 있다고는 할 수 없겠지만, 그 점은 제쳐놓고 다음과 같은 클라이언트 코드에서 어떤 일이 일어나는지 생각해 봅시다.
Widget w;
...
auto vals1 = w.data(); // w.values를 vals1에 복사
Widget::data의 반환 형식은 왼값왼 값 참조(정확히는 std::vector&)이고 왼 값 참조는 정의상 왼 값으로 취급되므로, 이 코드는 하나의 왼 값으로 vals1을 초기화합니다. 따라서, 주석에도 나와 있듯이 vals1은 w.values로부터 복사 생성됩니다.
(* '복사 생성하다(copy-construct)'는 객체를 복사 생성자를 이용해서 생성하는 것을 뜻합니다. 마찬가지로 '복사 배정하다(move-assign)'는 복사 배정 연산자를 이용해서 배정하는 것을 뜻합니다.)
다음으로, Widget을 생성하는 팩터리 함수가 있다고 합시다.
Widget makeWidget();
그리고 이 makeWidget이 돌려준 Widget 객체 안의 std::vector를 이용해서 변수를 초기화한다고 합시다.
auto vals2 = makeWidget().data(); // Widget 안에 있는 values를 vals2에 복사
이번에도 Widgets::data는 왼값왼 값 참조를 돌려주며, 언제나 왼 값 참조는 하나의 왼 값이므로, 역시 이번에도 새 객체(vals2)는 Widget 안의 values로부터 복사 생성됩니다. 그러나 이번에는 Widget이 makeWidget이 돌려준 임시 객체(즉, 오른 값)입니다. 따라서 그 임시 객체 안의 std::vector를 복사하는 것은 시간 낭비입니다. 복사보다는 이동이 바람직하지만, data가 왼 값 참조를 돌려주기 때문에 C++의 규칙들을 준수하는 컴파일러는 반드시 복사 연산을 위한 코드를 작성해야 합니다. (소위 "겉보기 규칙(as if rule)"이라는 것을 통해서 어느 정도 최적화할 여지가 있긴 하지만, 컴파일러가 그 점을 활용하리라고 기대하고 저런 코드를 작성하는 것은 그리 현명하지 않은 일입니다.)
정말로 필요한 것은 data가 오른값 Widget에 대해 호출된 경우에는 반드시 오른 값을 돌려주게 하는 것입니다. data를 왼 값 Widget과 오른 값 Widget에 대해 개별적으로 중복 적재하면 그런 일이 가능합니다.
(* 겉보기 규칙(as if rule) : 외부에서 관찰할 수 있는 프로그램 행동이 변하지 않는 한 컴파일러가 임의의 최적화를 적용할 수 있다는 규칙입니다. "외부에서 관찰할 수 있는 프로그램 행동"의 구체적인 의미는 다소 난해한데, 관련 자료를 찾아보기 바랍니다. 이 규칙이 적용되는 대표적인 예는 C++98 시절 원시적인 형태의 이동 의미론이라 할 수 있는 반환 값 최적화(RVO; Chapter(25) 참고)입니다.)
'컴퓨터과학' 카테고리의 다른 글
[Effective C++] (13-2) iterator보다 const_iterator를 선호할 것 (0) | 2020.05.31 |
---|---|
[Effective C++] (12-4) ~ (13-1) iterator보다 const_iterator를 선호할 것 (0) | 2020.05.31 |
[Effective C++] (12-2) 재정의 함수들을 override로 선언할 것 (0) | 2020.05.29 |
[Effective C++] (12-1) 재정의 함수들을 override로 선언할 것 (0) | 2020.05.29 |
[Effective C++] (11-2) 정의되지 않은 비공개 함수보다 삭제된 함수를 선호할 것 (0) | 2020.05.28 |