본문 바로가기
프로그래밍언어/C++

[Effective C++] 항목 35~37

by 목가 2018. 4. 10.
반응형
항목 35. 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자

ex)
class GameCharacter
{
 public:
virtual int healthValue() const;         //캐릭터 체력치 반환 함수
}

가상함수를 대체할 4가지 패턴 방식


1.비가상 인터페이스 관용구(Non-Virtual Interface 관용구)를 통한 탬플릿 메서드 패턴

메서드 패턴이란?


class GameCharacter
{
 public:
<b>int healthValue() const;              </b>//캐릭터 체력치 반환 함수
{
  ... //사전 동작
  int reVal = <b>doHealthValue</b>();
 ... //사후 동작    ex) mutex
}
...
  private:
virtual int doHealthValue() const      //파생클래스가 재정의하여 사용가능.
{
  ...
}
}

==> 공통적인 부분은 부모클래스에서 정의하여 중복 최소화,
     자식클래스에서 세부적 다른 구현이 필요한 것들은 재정의하여 구현.

private의 virtual 함수를 재정의한다는 의미 = 자식클래스는 사용이 불가능하므로 동작에 대한 구현 권한을 가짐.
private virtual 함수는 부모클래스만 사용 가능하므로 동작 시점에 대한 권한을 가짐.


2.함수 포인터로 구현한 전략 패턴

캐릭터 체력 계산 함수를 캐릭터 타입과 별개로 한다.
-> 캐릭터 타입의 생성자에 체력 계산 함수 포인터를 전달

전략패턴이란?



class GameCharacter;

int defaultHealthCalc(const GameCharacter& gc);        // gamecharacter 객체를 받아서 체력 계산 후 출력

class GameCharacter
{
 public:
typedef int (*HealthCalcFunc) (const GameCharacter&);          //함수 포인터 typedef 선언

explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)  // 이 부분 : 위 전략패턴 설명의 동적 알고리즘 교체
healthFunc(hcf)
{ }

int healthValue() const
{ return healthFunc(*this); }

private:
HealthCalcFunc healthFunc;
};

==> 하지만 체력 계산 함수가 클래스의 맴버로부터 빠져나왔으므로 public 이외의 private data 들의 접근이 불가능하여 구현에 어려움이 생길 수 있다. 이를 해결하기위해 private data를 public 으로 바꾸는 등의 캡슐화 정도를 떨어뜨리게 된다. 

캡슐화를 떨어뜨려도 이 패턴의 이점(실행 도중 알고리즘 변경 등)이 크다면 이 전략패턴을 사용한다.


3.tr1::function으로 구현한 전략 패턴

전략패턴의 함수 포인터를 tr1::function 타입의 객체를 써서 대체한다.

tr1::function 타입의 객체란?
 -> 함수호출성객체(callable entitiy) (함수포인터, 함수 객체, 맴버 함수 포인터)를 가질 수 있다.

class GameCharacter;

int defaultHealthCalc(const GameCharacter& gc);        // gamecharacter 객체를 받아서 체력 계산 후 출력

class GameCharacter
{
 public:
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
                               ㄴ반환형    ㄴ매개변수   //반환형, 매개변수로 명시적/암시적 변환 가능한 타입 수용 가능.
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
healthFunc(hcf)
{ }

int healthValue() const
{ return healthFunc(*this); }

private:
<b>HealthCalcFunc </b><font color="#3a32c3">healthFunc</font>;
};

// 사용 예제. p262


4."고전적인" 전략 패턴

체력치 계산 함수를 나타내는 클래스 계통을 따로 만들고, 실제 체력치 계산 함수는 이 클래스 계통의 가상 멤버함수로 만든다.

// 그림 p265.

class GameCharacter;

class HealthCalcFunc
{
 public:
virtual int calc(const GameCharacter& gc) const
{ ... }
};

//자식클래스 : 상속해서 calc함수 재정의

HealthCalcFunc defaultHealthCalc;

class GameCharacter
{
 public:
explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)    //자식클래스 객체 삽입
: phealthCalc(phcf)
{ }

int healthValue() const
{ return phealthCalc->calc(*this); }

private:
HealthCalcFuncp<font color="#3a32c3">healthCalc;</font>
};

*요약

- 비가상 인터페이스 관용구 사용: 공개되지 않은 가상 함수를 비가상 public 멤버 함수로 감싸서 호출하는 템플릿 메서드 패턴의 한 형태
- 가상 함수를 함수 포인터 멤버로 대체한다: 군더더기 없이 전략 패턴의 핵심만을 보여주는 형태
- 가상 함수를 tr1::function 데이터 멤버로 대체하여, 호환되는 시그니처를 가진 함수호출성 개체를 사용할 수 있도록 만듭니다: 전략패턴의 한 형태
- 한쪽 클래스 계통에 속해 있는 가상 함수를 다른 쪽 계통에 속해있는 가상 함수로 대체: 전략 패턴의 전통적 구현 형태



항목 36. 상속받은 비가상 함수를 파생클래스에서 재정의하는 것은 절대 금물!


class B
{
  public: 
void mf();
...
}

class D : public B{...};

D x;

B *pB = &x;
pB->mf();

D * pD = &x;
pD->mf();

- 둘 다 객체가 갖으므로 같은 함수를 호출하지만 다음의 예에서는 다른 함수를 호출(황당한 동작)하게 된다.
  - mf()가 비가상 함수이고 D 클래스가 mf 함수를 또 정의할 경우.

Class D: public B
{
  public:
void mf();
...
};

pB->mf();     // B클래스의 mf()를 호출
pD->mf();     // D클래스의 mf()를 호출

- 같은 객체를 가리키지만 다른 함수를 호출하는 이상동작 발생.

이유? 비가상 함수는 정적 바인딩으로 묶이기 때문이다. pB->mf()는 B클래스 타입으로 선언되었기 때문에 함수의 위치가 항상 B클래스에 정의되어 있을 것이라고 결정된다.

위 처럼 public 상속일 경우 "D is B"의 의미가 된다. 그런데 비가상함수는 클래스 파생에 상관없이 불변동작을 정해두는 것인데, 파생클래스에서 비가상함수를 재정의해버리면 "D is B"가 거짓이 된다.

이것만은 잊지말자
* 상속받은 비가상 함수를 재정의하는 일은 절대로 하지 맙시다.

항목 37. 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자

class Shape
{
  public:
enum ShapeColor{Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const = 0;
};

class Rectangle: public Shape
{
  public:
virtual void draw(ShapeColor color = Green) const;
};

class Circle: public Shape
{
  public:
virtual void draw(ShapeColor color) const;
};

부모 클래스 : Shape
자식 클래스 : Rectangle, Circle

Shape* ps;                      //정적타입:Shape*, 동적타입: 없음
Shape* pc = new Circle;       //정적타입:Shape*, 동적타입: Circle
Shape* pr = new Rectangle;   //정적타입:Shape*, 동적타입: Rectangle


pr->draw();  //Rectangle::draw(Shape::Red) 호출
 - pr객체인 Rectangle의 draw함수가 호출되지만(동적 바인딩)
   기본 매개변수는 정적 바인딩되므로 Shape의 기본 매개변수가 호출된다.


ex) 기본매개변수를 똑같이 할 경우
class Shape
{
  public:
enum ShapeColor{Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const = 0;
};

class Rectangle: public Shape
{
  public:
virtual void draw(ShapeColor color = <font color="#ff0000"><b>Red</b></font>) const;
};

- 코드중복, 의존성(부모 draw 기본매개변수를 바꾸면 자식 draw의 것도 바꿔줘야한다.)

해결방법: 비가상 인터페이스 관용구(NVI 관용구)
 - 자식 클래스에서 재정의할 수 있는 가상 함수를 private으로 만든 후 비가상 public 함수로 호출해서 사용

class Shape
{
  public:
enum ShapeColor{Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const
{
   doDraw(color);
}
  private:
virtual void doDraw(ShapeColor color) const = 0;
};

class Rectangle: public shape
{
 public: ...
 private:
virtual void doDraw(ShapeColor color) const;
}


반응형

'프로그래밍언어 > C++' 카테고리의 다른 글

[Effective C++] 항목 41~43  (0) 2018.04.10
[Effective C++] 항목 38~40  (0) 2018.04.10
[Effective C++] 항목 29~31  (0) 2018.04.10
[Effective C++] 항목 26~28  (0) 2018.04.10
[Effective C++] 항목 22 ~ 25  (0) 2018.01.23

댓글