이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 250p ~ 261p
이펙티브 C++(250p ~ 261p)
요약
항목 34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자
- 인터페이스 및 기본 구현을 제공하는 함수를 별도로 마련하는 방법을 별로 좋아하지 않을 수도 있다.
- 이를 위해 순수 가상 함수가 파생 클래스에서 재선언되어야 한다는 사실을 활용하되, 자체적으로 순수 가상 함수의 구현을 준비해 둘 수 있다.
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
}
void Airplane::fly(const Airport& destination) {
// ...
}
class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination) {
Airplane::fly(destination);
}
}
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination);
}
void ModelB::fly(const Airport& destination) {
// ..
}
- 멤버 함수가 비가상 함수로 되어 있다는 것은, 이 함수는 파생 클래스에서 다른 행동이 일어날 것으로 가정하지 않았다는 뜻이다.
- 비가상 함수를 선언하는 목적은 파생 클래스가 함수 인터페이스와 더불어 그 함수의 필수적인 구현을 물려받게 하는 것이다.
- 비가상 함수는 클래스 파생에 상관없는 불변동작과 같기 때문에 재정의할 수 있는 수준의 것이 아니다.
class Shape {
public:
int objectID() const;
}
항목 35: 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자
- 게임 개발팀에서 각종 캐릭터를 클래스로 구현하고 있는 상황이다.
- 캐릭터의 체력이 얼마나 남았는지를 나타내는 정수 값을 반환하는 함수가 있다.
- 이 함수는 캐릭터마다 체력을 계산하는 방식이 다를 것이 확실하므로 가상 함수로 선언해놓았다.
class GameCharacter {
public:
virtual int healthValue() const;
};
비가상 인터페이스 관용구를 통한 템플릿 메서드 패턴
- 이 방법은 가상 함수는 반드시 private 멤버로 만들어 은폐시켜야 한다는 생각을 기반으로 시작한다.
- 이 방법에 따르면
healthValue
는 public으로 그대로 두되 비가상 함수로 선언하고, 내부에 실제 동작을 맡은 private 가상 함수를 호출하는 식으로 구현된다.
class GameCharacter {
public:
// 파생 클래스에서 재정의 불가
int healthValue() const
{
// 사전동작
int retVal = doHealthValue();
// 사후동작
}
private:
// 파생 클래스에서 재정의 가능
virtual int doHealthValue() const
{
}
};
- 사용자로 하여금 public 비가상 멤버 함수를 통해 private 가상 함수를 간접적으로 호출하게 만드는 방법으로, 비가상 함수 인터페이스 관용구(non-virtual interface: nvi)라고 알려져 있다.
- NVI는 템플릿 메서드 패턴을 C++ 방식으로 구현한 것이다.
- 필자는
healthValue
메서드를doHealthValue
의 래퍼(wrapper)라고 부른다.
- 필자는
- NVI 관용구에서 사전동작과 사후동작이 있는데, 가상 함수를 호출된 후에 어떤 상태를 없애는 작업이 래퍼 공간에서 보장된다는 장점을 만든다.
- 가상 함수를 재정의하는 일은 어떤 동작을 어떻게 구현할 것인가를 지정하는 것이다.
- 가상 함수를 호출하는 일은 그 동작이 수행될 시점을 지정하는 것이다.
- 따라서 NVI 관용구에서 가상 함수가 어떤 기능을 수행할지는 파생 클래스가 가지는 권한이 되지만, 언제 호출할지 결정하는 것은 기본 클래스의 권한이 된다.
함수 포인터로 구현한 전략 패턴
- NVI 관용구는 public 가상 함수를 대신할 수 있는 괜찮은 방법이지만, 클래스 설계 관점에서는 눈속임에 불과하다.
- 좀 더 설계적인 측면에서 캐릭터의 체력치를 계산하는 작업은 캐릭터의 타입과 별개로 놓는 것이 좋을 수 있다.
- 이는 곧 체력 계산이 캐릭터의 일부일 필요가 없다는 말이 된다.
- 따라서 각 캐릭터의 생성자에 체력 계산용 함수의 포인터를 넘기고, 이 함수를 호출하는 것이다.
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter& gc);
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(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);
- 또한 런타임 도중에 특정 캐릭터에 대한 체력치 계산 함수를 바꿀 수 있다.
- 체력치 계산 함수가 캐릭터의 일부가 아니게 되었으므로 클래스의 비공개 데이터는 접근이 불가능해진다.
- public 영역에 없는 부분을 비멤버 함수도 접근할 수 있게 하려면 그 클래스의 캡슐화를 약화시키는 방법밖에 없는 것이 일반적인 법칙이다.
- 비멤버 함수를 프렌드로 선언
- 함수 포인터를 통해 얻는 이점들이 과연 클래스의 캡슐화를 떨어뜨리면서 얻는 불이익을 채워줄지 아닐지는 설계를 보면서 스스로 판단해야 한다.
메모
항목 34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자
- 인터페이스 상속은 구현 상속과 다르다. public 상속에서 파생 클래스는 항상 기본 클래스의 인터페이스를 모두 물려받는다.
- 순수 가상 함수는 인터페이스 상속만을 허용한다.
- 단순 가상 함수는 인터페이스 상속과 더불어 기본 구현의 상속도 가능하도록 지정한다.
- 비가상 함수는 인터페이스 상속과 더불어 필수 구현의 상속도 가하도록 지정한다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(270p ~ 279p) (0) | 2024.02.02 |
---|---|
이펙티브 C++(261p ~ 270p) (0) | 2024.02.01 |
이펙티브 C++(230p ~ 239p) (0) | 2024.01.30 |
이펙티브 C++(240p ~ 250p) (0) | 2024.01.30 |
이펙티브 C++(230p ~ 239p) (0) | 2024.01.29 |