이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 261p ~ 270p
이펙티브 C++(261p ~ 270p)
요약
항목 35: 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자
tr1::function으로 구현한 전략 패턴
- tr1::function 타입의 객체를 써서 함수 포인터를 대신하게 만들 수 있다.
- tr1::function은 함수호출성 개체(callable entity)를 가질 수 있고, 이들 개체는 주어진 시점에서 예상되는 시그니처와 호환되는 시그니처를 가지고 있다.
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
// 함수호출성 개체로서, GameCharacter와 호환되는 어떤 것이든 넘겨받아서 호출될 수 있음
// int와 호환되는 모든 타입의 객체를 반환함
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf) {}
int healthValue() const { return healthFunc(*this); }
private:
HealthCalcFunc healthFunc;
}
- HealthCalcFunc는 tr1::function 템플릿을 인스턴스화한 것에 대한 typedef 타입이다.
- 이 타입은 일반화된 함수 포인터 타입처럼 동작한다.
- HealthCalcFunc가 원래 어떤 것을 typedef 했는지를 보자.
std::tr1::function<int (const GameCharacter&)>
- 대상 시그니처는
const GameCharacter&
레퍼런스를 받고 int를 반환하는 함수이다.- 해당 tr1::function 타입으로 만들어진 객체는 대상 시그니처와 호환되는 함수호출성 개체를 어떤 것도 가질 수 있다.
- 매개변수로 const GameCharacter&이거나 이 타입으로 암시적 변환이 가능한 타입을 말한다.
- 반환타입도 암시적으로 int로 변환될 수 있다.
- 해당 tr1::function 타입으로 만들어진 객체는 대상 시그니처와 호환되는 함수호출성 개체를 어떤 것도 가질 수 있다.
#include <functional>
using namespace std;
class GameCharacter;
int defaultHealthFunc(const GameCharacter&);
class GameCharacter {
public:
typedef function<int(const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthFunc) :
healthFunc(hcf)
{}
private:
HealthCalcFunc healthFunc;
};
short calcHealth(const GameCharacter&);
// 호출 가능한 객체
struct HealthCalculator {
int operator() (const GameCharacter&) const {};
};
class GameLevel {
public:
float health(const GameCharacter&) const;
};
class EvilBadGuy : public GameCharacter
{
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthFunc)
: GameCharacter(hcf)
{}
};
class EyeCandyCharacter : public GameCharacter
{
public:
explicit EyeCandyCharacter(HealthCalcFunc hcf = defaultHealthFunc)
: GameCharacter(hcf)
{}
};
// 함수를 생성자에 전달하면 function 템플릿이 시그니처를 보고 암시적으로 function 객체를 생성한다.
EvilBadGuy ebg1(calcHealth);
// 호출 가능한 객체 전달
// function 템플릿이 받는 객체가 호출 가능한 객체라면 시그니처를 확인하고 function 객체를 생성한다.
EyeCandyCharacter ecc1((HealthCalculator()));
GameLevel currentLevel;
// GameLevel::health를 function 템플릿에 전달하기 위해 bind를 사용한다.
// 멤버 함수는 암시적으로 자기 자신인 this 포인터를 전달 받아야 하기 때문에 이를 위해 currentLevel을 넣어준다.
// placeholders::_n은 n번째 매개변수를 의미한다.
EvilBadGuy ebg2(bind(&GameLevel::health, currentLevel, placeholders::_1));
고전적인 전략 패턴
- C++의 기능을 깊게 파고드는 방법 말고 전통적인 디자인 패턴으로 전략 패턴을 구현할 수도 있다.
- 고전적인 전략 패턴을 적용하면 체력치 계산 함수의 기능을 담당하는 클래스 계통을 새로 만들고, 실제 체력치 계산 함수는 이 클래스의 가상 멤버 함수로 만든다.
class GameCharacter;
class HealthCalcFunc {
public:
virtual int calc(const GameCharacter&) const {}
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
:pHealthCalc(phcf)
{}
int health() const
{
return pHealthCalc->calc(*this);
}
private:
HealthCalcFunc* pHealthCalc;
};
- 이 방법은 표준적인 전략 패턴 구현 방법에 친숙한 경우에 빨리 이해할 수 있다.
- HealthCalcFunc 클래스 계통에 파생 클래스를 추가함으로써 기존의 체력치 계산 알고리즘을 조정할 수 있는 가능성도 있다.
항목 36: 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!
- 비가상 함수를 파생 클래스에서 자체적으로 또 정의하고 있으면 다음과 같은 동작이 실행된다.
class B {
public:
void mf();
};
class D : public B {
public:
// B의 mf를 가림
void mf();
};
D d;
B* pB = &d;
D* pD = &d;
int main()
{
// B::mf 호출
pB->mf();
// D::mf 호출
pD->mf();
}
- 이렇게 작동을 하는 이유는 비가상 함수는 정적 바인딩으로 묶이기 때문이다.
- 바인딩은 프로그램 소스에 쓰인 각종 내부 요소, 이름, 식별자들에 대해 값 혹은 속성을 확정하는 과정을 일컫는다.
- 이 과정이 빌드 중에 이루어지면 정적 바인딩이라고 하고, 실행 중에 이루어지면 동적 바인딩이라고 한다.
pB
는 B에 대한 포인터 타입으로 선언되어 있기 때문에 pB를 통해 호출되는 비가상 함수는 항상 B 클래스에 정의되어 있을 것이라고 빌드 중에 결정된다.(B의 파생 객체를 가리켜도 동일함)
- 가상 함수의 경우엔 동적 바인딩으로 묶인다.
- 따라서 파생 클래스를 만드는 도중에 기본 클래스로부터 물려받은 비가상 함수를 재정의하면, 파생 클래스는 일관성 없는 동작을 보이는 이상한 클래스가 된다.
public 상속은 is-a, 비가상 멤버 함수는 클래스 파생에 관계없는 불변동작
- B 객체에 해당되는 모든 것들이 D 객체에 그대로 적용된다. 왜냐하면 모든 D 객체는 B 객체의 일종이기 때문이다.
- B에서 파생된 클래스는 mf 함수의 인터페이스와 구현을 모두 물려받는다. mf는 B의 비가상 멤버 함수이기 때문이다.
- 같은 이유로 다형성을 부여한 기본 클래스의 소멸자를 반드시 가상 함수로 만들어 두어야 한다.
- 그렇게 하지 않으면 파생 클래스의 소멸자가 기본 클래스의 소멸자를 덮어버려 기본 클래스의 소멸자가 호출되지 않는 문제가 발생한다.
- 소멸자를 직접 선언하지 않았더라도 컴파일러가 자동으로 만들어 주는 소멸자에서 이러한 문제가 발생할 것이다.
메모
항목 35: 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자
- 가상 함수 대신에 쓸 수 있는 다른 방법으로 NVI 관용구 및 전략 패턴을 들 수 있다. 이 중 NVI 관용구는 그 자체가 템플릿 메서드 패턴의 한 예이다.
- 객체에 필요한 기능을 멤버 함수로부터 클래스 외부의 비멤버 함수로 옮기면, 그 비멤버 함수는 그 클래스의 public 멤버가 아닌 것들을 접근할 수 없다는 단점이 생긴다.
- tr1::function 객체는 일반화된 함수 포인터처럼 동작한다. 이 객체는 주어진 대상 시그니처와 호환되는 모든 함수호출성 개체를 지원한다.
항목 36: 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!
- 상속받은 비가상 함수를 재정의하는 일은 절대로 하지 말자.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(279p ~ 290p) (1) | 2024.02.03 |
---|---|
이펙티브 C++(270p ~ 279p) (0) | 2024.02.02 |
이펙티브 C++(250p ~ 261p) (0) | 2024.01.31 |
이펙티브 C++(230p ~ 239p) (0) | 2024.01.30 |
이펙티브 C++(240p ~ 250p) (0) | 2024.01.30 |