일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- resource management class
- 예외
- 게임
- 오블완
- 함수 객체
- 비교 함수 객체
- reference
- 참조자
- effective stl
- lua
- exception
- UE4
- operator new
- 암시적 변환
- 상속
- 다형성
- 영화 리뷰
- 언리얼
- implicit conversion
- 메타테이블
- 루아
- Effective c++
- 스마트 포인터
- c++
- 티스토리챌린지
- Smart Pointer
- virtual function
- 반복자
- 영화
- more effective c++
Archives
- Today
- Total
스토리텔링 개발자
[Effective C++] 35. 일반 가상 함수 외의 구현법 본문
728x90
항목 35. 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러두자.
가상 함수 버전
class GameCharacter
{
public:
virtual int healthValue() const; // 캐릭터의 체력 가상 함수
...
};
- 가상 함수 외의 방법으로 구현할 수는 없을까?
비가상 인터페이스(NVI) 관용구를 통한 템플릿 메서드 패턴
class GameCharacter
{
public:
int healthValue() const // 비가상 인터페이스
{
...
int retVal = doHealthValue();
...
return retVal;
}
...
private:
virtual int doHealthValue() const { ... } // private 가상 함수
}
- 가상 함수 은폐론
- 가상함수는 반드시 private 멤버로 두어야 한다.
- 비가상 함수 인터페이스 관용구 (NVI 관용구)
- public 비가상 멤버 함수를 통해 private 가상 함수를 간접적으로 호출하게 만드는 방법.
- 템플릿 메소드 디자인 패턴을 c++ 식으로 구현한 것이다.
- 비가상 함수는 가상 함수의 랩퍼(wrapper)이다.
- 이 경우, private 가상함수를 파생 클래스에서 재정의하여 사용할 것이다.
- private 이므로 파생 클래스에서는 재정의만 가능하지, 직접 사용할 수 없을 것이다. 모순이 아닐까?
- 하지만 재정의와 호출은 늘 함께 움직이는 개념이 아니므로, 모순이 아니다.
- 가상 함수 재정의 : 어떤 동작을 어떻게 구현할 것인가를 지정하는 것.
- 가상 함수 호출 : 그 동작이 수행될 시점을 지정하는 것.
- 파생 클래스에서 해당 함수의 호출까지 필요한 경우 protected로 가상함수를 제공하면 된다.
함수 포인터로 구현한 전략(Strategy) 패턴
class GameCharacter; // 전방 선언
// 체력치 계산에 대한 default 알고리즘
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&* gc);
explicit GameCharacter(HealthCalcFunc hef = defaultHealthCalc) : healthFunc(hcf) {}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc; // 함수 포인터
};
- 전략(Stratagy) 패턴
- 객체가 될 타겟 클래스는 인터페이스만 제공하고, 구현은 다른 객체에게 맡기는 방식으로, 구현을 인터페이스와 분리시키는 패턴
- 이 경우 구현을 다른 객체가 아니라 다른 함수에 맡겼다.
- 장점
- 같은 캐릭터 타입으로부터 만들어진 객체들도 체력치 계산 함수를 각각 다르게 가질 수 있다.
- 생성자에 각기 다른 로직을 전달하면 된다.
- 런타임에도 특정 캐릭터에 대한 체력치 계산 함수를 바꿀 수 있다.
- 같은 캐릭터 타입으로부터 만들어진 객체들도 체력치 계산 함수를 각각 다르게 가질 수 있다.
- 단점
- 체력치가 계산되는 대상 객체의 비공개 데이터는 이 함수가 직접 접근할 수 없다.
- 클래스의 캡슐화를 약화시키는 방법밖에 없다..
- 체력치가 계산되는 대상 객체의 비공개 데이터는 이 함수가 직접 접근할 수 없다.
function으로 구현한 전략 패턴
- 함수 포인터로 구현했을 때의 불만
- 체력치 계산을 왜 꼭 함수가 해야 하지?
- 멤버 함수는 왜 사용할 수 없지?
- 반환값을 반드시 int로 해야 하나? int로 암시적 변환이 되는 다른 타입으로 하고 싶은데.
- 그러므로 함수 포인터를 function 객체로 대체해보자.
- function 객체
- 함수 호출성 개체(함수 포인터, 함수 객체, 멤버 함수 포인터)를 가질 수 있다.
- 주어진 시점에서 예상되는 시그니처와 호환되는 시그니처를 갖고 있다.
class GameCharacter; // 전방 선언
int defaultHealthCalc(const GameCharacter& gc); // 기본 계산 함수
class GameCharacter
{
public:
// std::function을 사용하도록 변경
typedef std::function<int(const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
- 이로써 대상 시그니처와 호환되는 함수 호출성 개체를 어떤 것도 가질 수 있다.
- 호환된다는 말의 뜻
- 매개변수 값 : const GameCharacter&이거나 암시적 변환이 가능한 타입.
- 반환값 : int이거나 암시적으로 int로 변환되는 타입.
- 호환된다는 말의 뜻
고전적인 전략 패턴
class GameCharacter; // 전방 선언
// 로직 클래스
class HealthCalcFunc
{
public:
...
virtual int calc(const GameCharacter& gc) const { ... }
...
};
// 기본 계산
HealthCalcFunc defaultHealthCalc;
class GameCharacter
{
public:
explicit GameCharacter(HealthCalcFunc* phef = &defaultHealthCalc) : pHealthCalc(phef) {}
int healthValue() const
{
return pHealthCalc->clac(*this);
}
...
private:
HealthCalcFunc* pHealthCalc;
- 함수로 제공한 위의 사례와 달리, 구현 클래스를 따로 마련하여 주었다.
- 장점
- 표준 전략 패턴 구현 방법에 친숙한 경우 쉽게 이해할 수 있는 구조 설계
- 구현 클래스의 파생 클래스를 추가하여 계산 알고리즘을 조정 / 개조할 수 있는 가능성이 열린다.
728x90
'개발 > Effective C++' 카테고리의 다른 글
[Effective C++] 37. 상속된 함수의 매개변수 기본값 재정의 문제 (0) | 2024.07.05 |
---|---|
[Effective C++] 36. 상속된 비가상 함수 이름 가리기 문제 (0) | 2024.07.05 |
[Effective C++] 34. 인터페이스 상속과 구현 상속의 차이 (0) | 2024.07.03 |
[Effective C++] 33. 상속된 이름 가리기 문제 (0) | 2024.07.02 |
[Effective C++] 32. public 상속은 "is-a" (0) | 2024.06.28 |
Comments