스토리텔링 개발자

[Effective C++] 35. 일반 가상 함수 외의 구현법 본문

개발/Effective C++

[Effective C++] 35. 일반 가상 함수 외의 구현법

김디트 2024. 7. 4. 11:27
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) 패턴
    • 객체가 될 타겟 클래스는 인터페이스만 제공하고, 구현은 다른 객체에게 맡기는 방식으로, 구현을 인터페이스와 분리시키는 패턴
    • 이 경우 구현을 다른 객체가 아니라 다른 함수에 맡겼다.
  • 장점
    1. 같은 캐릭터 타입으로부터 만들어진 객체들도 체력치 계산 함수를 각각 다르게 가질 수 있다.
      • 생성자에 각기 다른 로직을 전달하면 된다.
    2. 런타임에도 특정 캐릭터에 대한 체력치 계산 함수를 바꿀 수 있다.
  • 단점
    1. 체력치가 계산되는 대상 객체의 비공개 데이터는 이 함수가 직접 접근할 수 없다.
      • 클래스의 캡슐화를 약화시키는 방법밖에 없다..

 

 

 

function으로 구현한 전략 패턴
  • 함수 포인터로 구현했을 때의 불만
    1. 체력치 계산을 왜 꼭 함수가 해야 하지?
    2. 멤버 함수는 왜 사용할 수 없지?
    3. 반환값을 반드시 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
Comments