티스토리 뷰

 

Effective C++ 이펙티브 C++
스콧 마이어스 저/곽용재

 

6. 상속, 그리고 객체 지향 설계 (2)

 

항목 35 : 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자

 

가상 함수 대신 쓸 수 있는 매서드 패턴 네 가지를 알아보자.

class GameCharacter

{

public:

// 캐릭터의 체력치를 반환하는 함수.

// 파생 클래스를 이 함수를 재정의할 수 있습니다.

virtual int healthValue() const;

};

 

1. 비가상 인터페이스 관용구(NVI 관용구)를 통한 템플릿 메서드 패턴

공개되지 않은 가상 함수를 비가상 public 멤버 함수로 감써서 호출하는, 템플릿 메서드 패턴의 한 형태입니다.

 

class GameCharacter

{

public:

// 파생 클래스는 이제 이 함수를 재정의 불가

int healthValue() const

{

// "사전" 동작 수행

...

// 실제 동작 수행

int retVal = doHealthValue();

// "사후" 동작 수행

...

return retVal;

}

...

private:

// 파생 클래스는 이 함수를 재정의 가능

virtual int healthValue() const

{

// 캐릭터의 체력치 계산을 위한 기본 알고리즘 구현

...

}

};

 

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

가상 함수를 함수 포인터 데이터 멤버로 대체합니다.

 

// 전방 선언

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;

};

 

2.1. 장점

2.1.1. 같은 캐릭터 타입으로부터 만들어진 객체(인스턴스)들도 체력치 계산 함수를 각각 다르게 가질 수 있습니다.

class EvilBadGuy : public GameCharacter

{

public:

explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf)

{ ... }

...

};

 

// 다른 동작 원리로 구현된 체력치 계산 함수들

int loseHealthQuickly(const GameCharacter&);

int loseHealthSlowly(const GameCharacter&);

 

// 같은 타입인데도 체력치 변화가 다게 나오는 케릭터들

EvilBadGuy ebg1(loseHealthQuickly);

EvilBadGuy ebg2(loseHealthSlowly);

 

2.1.2. 게임이 실행되는 도중에 특정 캐릭터에 대한 체력치 계산 함수를 바꿀 수 있습니다.

 

2.2. 단점

2.2.1. 체력치가 계산되는 대상 객체의 비공개 데이터는 이 함수로 접근할 수 없습니다.

 

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

가상 함수를 tr1::function 데이터 멤버로 대체하여, 호환되는 시그니처를 가진 함수호출성 개체를 사용할 수 있도록 만듭니다.

 

class GameCharacter;

int defaultHealthCalc(const GameCharacter& gc);

 

class GameCharacter

{

public:

// HealthCalcFunc는 함수호출성 개체로서, 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;

};

 

// 체력치 계산 함수

short calcHealth(const GameCharacter&);

 

struct HealthCalculator

{

// 체력치 계산용 함수 객체를 만들기 위한 클래스

int operator() (const GameCharacter&) const

{ ... }

};

 

class GameLevel

{

public:

// 체력치 계산에 쓰일 멤버 함수

float health(const GameCharacter&) const;

...

};

 

class EvilBadGuy : public GameCharacter

{ ... };

 

// 또 하나의 캐릭터 타입. 생성자는 EvilBadGuy와 동일

class EyeCandyCharacter : public GameCharacter

{ ... };

 

// 체력치 계산을 위한 함수를 사용하는 캐릭터

EvilBadGuy ebg1(calcHealth);

// 체력치 계산을 위한 함수 객체를 사용하는 캐릭터

EyeCandyCharacter ecc1(HealthCalculator());

 

GameLevel currentLevel;

...

// ebg2의 체력치 계산 함수는 항상 currentLevel만을 GameLevel 객체로 쓴다고 지정

EvilBadGuy ebg2(

std::tr1::bind(&GameLevel::health, currentLevel, _1)

);

 

[참고] Boost.Bind http://www.boost.org/doc/libs/1_58_0/libs/bind/doc/html/bind.html

 

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

한쪽 클래스 계통에 속해 있는 가상 함수를 다른 계통에 속해 있는 가상 함수로 대체합니다.

 

class GameCharacter;

class HealthCalcFunc

{

public:

...

virtual int calc(const GameCharacter& gc) const

{ ... }

...

};

 

HealthCalcFunc defaultHealthCalc;

class GameCharacter

{

public:

explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc) : pHealthCalc(phcj)

{ }

int healthValue() const

{ return pHealthCalc->calc(*this); }

...

private:

HealthCalcFunc *pHealthCalc;

};

 

  • 가상 함수 대신에 쓸 수 있는 다른 방법으로 NVI 관용구 및 전략 패턴을 들 수 있습니다. 이 중 NVI 관용구는 그 자체가 템플릿 메서드 패턴의 한 예입니다.
  • 객체에 필요한 기능을 멤버 함수로부터 클래스 외부의 비멤버 함수로 옮기면, 그 멤버 함수는 그 클래스의 public 멤버가 아닌 것들을 접근할 수 없다는 단점이 생깁니다.
  • tr1:;function 객체는 일반화된 함수 포인터처럼 동작합니다. 이 객체는 주어진 대상 시그너처와 호환되는 모든 함수호출성 개체를 지원합니다.

 

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

 

class B

{

public:

void mf();

...

};

 

class D : public B { ... };

 

D x;

B *pB = &x;

pB->mf(); // D::mf 호출되어야 합니다.

 

D *pD = &x;

pD->mf(); // D::mf 호출되어야 합니다.

 

  • B 객체에 해당되는 모든 것들이 D 객체에 그대로 적용됩니다. 왜냐하면 모든 D 객체는 B 객체의 일종이기 때문입니다.
  • B에서 파생된 클래스는 mf 함수의 인터페이스와 구현을 모두 물려받게 됩니다. mf는 B 클래스에서 비가상 멤버 함수이기 때문입니다.

 

비가상 함수를 파생 클래스에서 재정의하면 비가상 함수는 정적 바인딩을 하기 때문에 public 상속 설계에 모순이 생깁니다.

class D : public B

{

public:

// B::mf를 가림.(항목 33 참조)

void mf();

...

};

 

pB->mf(); // B::mf 호출

pD->mf(); // D::mf 호출

 

  • 상속받은 비가상 함수를 재정의하는 일은 절대로 하지 맙시다.

 

 

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

 

1. 문제 - 상속 받은 기본 매개변수 재정의

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 = 0;

...

 

};

 

class Circle : public Shape

{

public:

virtual void draw(ShapeColor color) const;

...

};

 

파생 클래스에 정의된 가상 함수를 호출하면서 기본 클래스에 정의된 기본 매개변수 값을 사용해 버릴 수 있습니다. 가상 함수는 동적으로 바인딩되어 있지만 기본 매개변수는 정적으로 바인딩되어 있기 때문입니다.

// 모두  정적 타입 =  Shape*

Shape *ps;

Shape *pc = new Circle;

Shape *pr = new Rectangle;

 

// Circle::draw(Shape::Red)를 호출합니다.

pc->draw(Shape::Red);

// Rectangle::draw(Shape::Red)를 호출합니다.

pr->draw(Shape::Red);

// Rectangle::draw(Shape::Red)를 호출합니다!

 

pr->draw();

 

 

2. 해결 - 비가상 인터페이스(non-virtual interface) 관용구

class Shape

{

public:

enum ShapeColor {Red, Green, Blue};

// 이제는 비가상 함수입니다.

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;

...

};

 

  • 상속받은 기본 매개변수 값은 절대로 재정의해서는 안 됩니다. 왜냐하면 기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수(오버라이드할 수 있는 유일한 함수)는 동적으로 바인딩되기 때문입니다.

 

 

- James Song

댓글