[Effective C++] 항목 35 ~ 37
항목 35 : 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러두자.
- 비가상 인터페이스 관용구를 통한 템플릿 메서드 패턴
- 가상 함수 은폐론 : 가상함수는 반드시 private 멤버로 두어야 한다.
- 가상함수로 제공하던 함수(public)를 비가상 함수로 선언하고 내부적으로는 실제 동작을 맡은 private 가상함수를 호출하는 식으로 구현
- 비가상 함수 인터페이스 관용구(NVI 관용구)
- 템플릿 메소드 디자인 패턴을 c++ 식으로 구현한 것. »+ 가상 함수의 래퍼 »+ 사전, 사후 동작을 기본구현 해줄 수 있다.
- NVI 관용구에서 private 가상함수를 파생클래스에서 재정의하는 것은 무슨 의미?(재정의해 놓고 호출할 수도 없는데?)
- 가상 함수 재정의 : 어떤 동작을 어떻게 구현할 것인가 »+ 가상 함수 호출 : 그 동작이 수행될 시점을 지정
- 함수 포인터로 구현한 전략 패턴
- 캐릭터의 체력치를 계산하는 작업은 캐릭터의 타입과 별개로 하자.
- 각 캐릭터의 생성자에 체력치 계산용 함수의 포인터를 넘기게 만들고, 이 함수를 호출해서 실제 계산을 수행하게 한다.
- ex)
class GameCharacter; // 전방 선언 int defaultHealthCalc(const GameCharacter& gc); class GameCharacter { public: typedef int (*HealthCalcFunc)(const GameCharacter&); explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {} int healthValue() const { return healthFunc(*this); } .... private: HealthCalcFunc healthFunc; };
- 전략 패턴의 단순한 응용이다.
- 가상 함수에 비해 융통성을 가진다.
- 같은 파생 클래스 객체도 체력치 계산 함수가 각각 다르게 할 수 있다.
- 런타임 중 특정 캐릭터의 체력치 계산 함수를 교체할 수 있다.
- 단점
- 대상 객체의 비공개 데이터는 이 함수로 접근할 수 없다.
- 그 클래스의 캡슐화를 약화시키는 방법밖에 없다는 것이 일반적인 법칙이다. »1. 비멤버 함수를 friend로 선언한다. »2. 숨겨놓는 게 나을지 모르는 세부구현사항 접근자 함수를 public으로 한다.
- tr1::function으로 구현한 전략 패턴
- tr1::function 개열 객체
- 함수 호출성 개체(함수 포인터, 함수 객체, 멤버 함수 포인터)를 가질 수 있다.
- 주어진 시점에서 예상되는 시그니처와 호환되는 시그니처를 갖고 있다.
- ex) 앞의 예제에서 typedef int (*HealthCalcFunc)(const GameCharacter&);
- => typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
- 대상 시그니처(ex) int(const GameCharacter&) : const GameCharacter의 참조자를 받고 int를 반환하는 함수)와 호환되는 함수 호출성 개체를 어떤 것도 가질 수 있다.
- 호환 : const GameCharacter&이거나 암시적 변환이 가능한 타입. 반환도 암시적으로 int 반환되는 타입
- 융통성 증가 : 호환되는 함수 호출성 객체를 모두 넣을 수 있다!
- tr1::bind : 매개변수를 여럿 받는 함수의 매개변수를 묶을 때 사용한다.
- tr1::function 개열 객체
- 고전적인 전략 패턴
- 체력치 계산 함수 클래스를 따로 구현하고 내부적으로 가상 멤버 함수를 만들어 캐릭터 클래스에 변수로 두고 사용한다.
- 표준적 전략 패턴 구현에 친숙하면 빨리 이해가 가능하다.
- 체력 계산 클래스의 파생 클래스를 추가함으로써 기존 체력치 계산 알고리즘을 조정 / 개조할 여지를 준다.
- 가상함수를 대신하는 방법 요약
- 비가상 인터페이스 관용구(NVI관용구) 사용
- 가상함수를 함수 포인터 데이터 멤버로 대체
- 가상함수를 tr1::function 데이터 멤버로 대체하여, 호환되는 시그니처를 가진 함수 호출성 개체를 사용할 수 있도록 함
- 다른 쪽 클래스에 속한 가상 함수로 대체
항목 36 : 상속받은 비가상 함수를 파생클래스에서 재정의하는 것은 금물
- 비가상 함수는 정적 바인딩으로 묶인다.(37)
- 가상 함수는 동적 바인딩으로 묶인다.(37)
- 비가상 함수 재정의시 포인터 타입에 따라 어떤 걸 호출할지가 갈린다.
- 기본 클래스 소멸자를 가상함수로 하지 않으면 파생클래스에서 비가상 소멸자를 재정의할 것이다.
항목 37 : 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
- 호출은 동적 타입의 함수로 되지만 매개변수는 정적 타입에서 가져온다.
- 런타입 효율 문제 : 기본 매개변수가 동적으로 바인딩되면 런타임 중 가상함수의 기본 매개변수 값을 결정할 방법을 컴파일러 쪽에서 마련한다. 이는 현재 메커니즘보다 느리고 복잡하다.
- 기본 매개변수 값을 동일하게 제공한다.
- ex) ```cpp class Shape { public: virtual void draw(ShapeColor color = Red) const = 0; }
class Rectangle : public Shape { public: virtual void draw(ShapeColor color = Red) const; } ``` >+ 코드 중복, 의존성 문제 : 부모 클래스 기본 매개변수를 바꾸면 일일이 바꾸어주어야 하므로 코드중복이다.
- 비가상 인터페이스 관용구(NVI 관용구)를 사용하자.