KoreanFoodie's Study
Effective C++ | 항목 35 : 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자 본문
Effective C++ | 항목 35 : 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자
GoldGiver 2022. 10. 25. 16:28
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 35 : 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자
핵심 :
1. 가상 함수 대신에 쓸 수 있는 다른 방법으로 NVI 관용구 및 전략 패턴을 들 수 있다. 이 중 NVI 관용구는 그 자체가 템플릿 메서드 패턴의 한 예이다.
2. 객체에 필요한 기능을 멤버 함수로부터 클래스 외부의 비멤버 함수로 옮기면, 그 비멤버 함수는 그 클래스의 public 멤버가 아닌 것들을 접근할 수 없다는 단점이 생긴다.
3. std::function 객체는 일반화된 함수 포인터처럼 동작한다. 이 객체는 주어진 대상 시그니처와 호환되는 모든 함수 호출성 개체를 지원한다.
체력을 계산하는 함수를 다음과 같이 만든다고 해 보자.
class GameCharacter
{
public:
// 파생 클래스는 이제 이 함수를 재정의할 수 없다
int healthValue() const
{
// 사전 동작 수행
int retVal = doHealthValue();
// 사후 동작 수행
return retVal;
}
private:
// 파생 클래스는 이 함수를 재정의할 수 있다
virtual int doHealthValue() const
{
// 캐릭터 체력치 계산을 위한 기본 알고리즘 구현
}
};
위와 같은 방식을 비가상 함수 인터페이스(non-virtual interface: NVI) 관용구라고 한다. 사실 이 관용구는 템플릿 메서드(Template Method) 라 불리는 고전 디자인 패턴을 C++ 식으로 구현한 것이다.
NVI 대신 함수 포인터로 구현한 전략 패턴을 살펴보자.
// 전방 선언
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;
};
이렇게 만들면, 같은 캐릭터 타입의 객체라도 다른 체력치 계산 함수를 가질 수 있다. 다음처럼 말이다.
class EvilBadGuy: public GameCharacter
{
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf)
{}
};
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);
EvilBadGuy ebg1(loseHealthSlowly);
EvilBadGuy ebg2(loseHealthQuickly);
물론 이와 같은 방식은, 체력치 계산을 위해 public 함수보다 더 보호 수준이 높은 함수를 활용할 수 없다는 단점을 가지고 있다.
세 번째로, std::function 으로 구현한 전략 패턴을 살펴보자.
// typedef int (*HealthCalcFunc)(const GameCharacter&);
typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
GameCharacter 의 함수 포인터 정의에서 위의 한 줄만 바꿔주면 된다! 위 함수 객체는 'const GameCharacter 에 대한 참조자를 받고 int 를 반환하는 함수' 가 된다. 이 객체는 앞으로 대상 시그니처와 '호환되는' 함수 호출성 개체를 가질 수 있으며, 이는 매개변수 타입이 'const GameCharacter&' 이거나 암시적으로 'const GameCharacter&' 로 변환이 가능한 타입이어도 된다는 뜻이다. 반환 타입도 암시적으로 int 로 변환되면 사용할 수 있다. 예시를 보자.
// 반환 타입이 short 인 함수도 사용할 수 있다.
short calcHealth(const GameCharacter&);
// 체력치 계산용 함수 객체를 만들기 위한 클래스
struct HealthCalculator
{
int operator()(const GameCharacter&) const {}
};
class GameLevel
{
public:
// 체력치 계산에 쓰일 멤버 함수
// 반환 타입이 int 가 아닌 float 이다
float health(const GameCharacter&) const;
};
class EvilBadGuy: public GameCharacter { ... };
class EyeCandyCharacter: public GameCharacter { ... };
EvilBadGuy ebg1(calcHealth);
EyeCandyCharacter ecc1(HealthCalculator());
GameLevel currentLevel;
EvilBadGuy ebg2(
std::bind(
&GameLevel::health,
currentLevel,
_1)
);
bind 문에 대해 설명을 덧붙이자면, ebg2 의 체력치를 계산하기 위해 GameLevel 클래스의 health 멤버 함수를 써야 하는데, 실제로는 GameLevel 객체 하나를 암시적으로 받으므로 매개 변수를 두 개 받는다. 이 객체는 this 포인터가 가리키는 것인데, 이 매개변수 두개(GameCharacter 및 GameLevel) 를 받는 함수를 매개변수 한 개(GameCharacter)만 받는 함수로 바꾸기 위해, GameLevel::health 함수가 호출될 때마다 currentLevel 이 사용되도록 묶어 주고 있는 것이다. "_1" 은 "ebg2 에 대해 currentLevel 과 묶인 GameLevel::health 함수를 호출할 때 넘기는 첫 번째 자리의 매개변수" 를 뜻한다!
마지막으로, "고전적인" 전략 패턴에 대해 알아보자. 체력치 계산 함수를 나타내는 클래스 계통을 아예 따로 만드는 방식이다.
// 전방 선언
class GameCharacter;
class HealthCalcFunc
{
public:
virtual int calc(const GameCharacter& gc) const { ... }
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter
{
public:
explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)
: pHealthFunc(phcf)
{}
int healthValue() const
{
return pHealthFunc->calc(*this);
}
private:
HealthCalcFunc *pHealthFunc;
};
어쨌든, 가상 함수를 대신하는 설계 4가지를 잘 고려해 보자!
- 비가상 인터페이스 관용구(NVI 관용구)
- 가상 함수를 함수 포인터 데이터 멤버로 대체
- 가상 함수를 std::function 데이터 멤버로 대체
- 전통적인 전략 패턴 사용(다른 쪽 계통에 속해 있는 가상함수로 대체)
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective C++ | 항목 37 : 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자 (0) | 2022.10.25 |
---|---|
Effective C++ | 항목 36 : 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물! (0) | 2022.10.25 |
Effective C++ | 항목 34 : 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자 (0) | 2022.10.25 |
Effective C++ | 항목 33 : 상속된 이름을 숨기는 일은 피하자 (0) | 2022.10.25 |
Effective C++ | 항목 32 : public 상속 모형은 반드시 "is-a(...는 ...의 일종이다)"를 따르도록 만들자 (0) | 2022.10.25 |