Item 32 : Make sure public inheritance models "is-a"
Object-Oriented programming의 가장 중요한 규칙은 public inheritance가 "is-a"를 의미한다는 것이다.
만약 Base class를 public으로 상속하는 Derived class가 있다고 가정하자.
모든 Derived class type의 객체는 Base class type의 객체가 되지만, 반대는 틀리다.
Base class는 Derived class보다 더 일반적인 개념을, Derived class는 Base class보다 더 specialized된 개념을 ㅍ현한다.
객체의 타입이 Base class인 객체가 사용되는 어떤 곳이든 Derived class 타입의 객체를 사용할 수 있다.
모든 Derived class는 Base class이지만, 반대는 아니다.
Item 33 : Avoid hiding inherited names
상속된 이름을 숨기는 것은 피하자.
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf2();
void mf3();
...
};
class Derived : public Base{
public:
virtual void mf1();
void mf4();
...
};
void Derived::mf4()
{
...
mf2();
}
위 코드에서, Derived::mf4를 호출하면, 컴파일러는 mf2를 찾는다.
local scope, Derived class가 포함되는 scope, Base class에 대한 scope, Base가 포함되는 namespace의 scope, global scope 순서대로 mf2를 찾는다.
만약, mf1과 mf3를 오버로드하고, mf3를 Derived에 추가한다면,
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived : public Base{
public:
virtual void mf1();
void mf3();
void mf4();
...
};
이 경우, base class에 있는 함수 이름 mf1와 mf3은 derived class의 mf1과 mf3에 의해 숨겨진다.
Base::mf1과 Base::mf3는 더이상 Derived에게 상속되지 않는다.
타입, 가상함수 여부 등에 상관없이 이름이 가려진다.
사실, public 상속을 사용할 때, 오버로드 버전의 상속을 막는것은 is-a 관계 위반이다.
public 상속은 Base class의 모든 것들이 Derived class에 적용되어야 한다.
using 선언을 사용해 숨겨진 이름을 드러낼 수 있다.
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived : public Base{
public:
using Base::mf1; // make al things in Base names mf1 and mf3
usint Base::mf3; // visable in Derived's scope
virtual void mf1();
void mf3();
void mf4();
...
};
void example(void){
Derived d;
int x;
d.mf1(); //Derived::mf1()
d.mf1(x); //Base::mf1(int)
d.mf2(); //Base::mf2()
d.mf3(); //Derived::mf3()
d.mf3(x); //Base::mf3(double)
}
어떤 Base class를 상속받을 때, 오버로드된 함수가 Base class에 있고, 이 함수들 중 몇개만 재정의하고 싶다면, 각 이름에 대해 using 선언을 해야 한다.
때때로 base class의 모든 함수를 상속하기를 원하지 않을 수 있다.
public inheritance는 빼고 생각해야 한다.
Base class의 public에 위치한 이름들은 derived class에서도 public에 있어야 한다.
Derived class가 Base class로부터 private 상속이 이루어지고, 매개변수가 없는 mf1함수만을 상속하기를 원하는 경우를 생각해보자.
이 경우 using 선언을 사용해 매개변수가 없는 mf1 버전 하나만 상속할 수 없다.
using 선언을 하면 그 이름에 해당하는 모든 것들이 Derived class로 상속된다.
이 경우, forwarding function(전달 함수)을 사용해야 한다.
class Base{
public:
virtual void mf1() = 0;
virtual void mf1(int);
};
class Derived : private Base{
public:
virtual void mf1() // forwarding function; implicitly
{
Base::mf1();
}
};
void example(void){
Derived d;
int x;
d.mf1(); //Derived::mf1() -> Base::mf1()
d.mf1(x); //error! Base::mf1() is hidden
}
정리하자면, Derived class의 이름들은 base class의 이름을 숨긴다.
public 상속에서 이것은 바람직하지 않다.
숨겨진 이름들을 보이도록 하기 위해서, using 선언을 사용하거나 forwarding 함수를 사용한다.
Item 34 : Differentiate between inheritance of interface and inheritance of implimentation
인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자.
멤버 함수 인터페이스는 항상 상속된다.
따라서, 만약 함수가 클래스에 적용된다면 derived class에도 항상 적용된다.
함수는 pure virtual function, simple(impure) virtual function, non-virtual function으로 구분할 수 있다.
1. pure virtual function
순수 가상 함수는 그것을 상속한 클래스에서 다시 재정의 되어야 한다.
전형적으로, 추상 클래스에서 정의되지 않는다.
순수 가상 함수를 선언하는 목적은 derived class가 오직 인터페이스만 상속하도록 하기 위함이다.
어떠한 default implementation도 제공되지 않는다.
우연히, 순수 가상 함수의 정의가 제공될 수 있다.
즉, 구현을 제공할 수 있고 컴파일러에서 문제가 발생하지 않는다.
클래스 이름과 함께 함수를 호출함으로써 함수를 호출할 수 있다. ex) Shape::draw();
2. Simple virtual function
Simple virtual function의 경우, 함수의 인터페이스를 상속하고, derived class가 override할 수 있는 구현을 제공한다.
단순한 가상 함수를 선언하는 목적은 derived class가 함수의 인터페이스 뿐만 아니라 default implementation을 상속하도록 하는 것이다.
만약 클래스가 어떤 특별한 것을 원하지 않는다면 default implementation을 사용한다.
그러나, 단순 가상 함수가 함수 인터페이스와 기본 구현을 한꺼번에 지정하도록 내버려 두는 것은 위험하다.
만약 다른 구현방법이 적용되어야 하는 경우, 이를 까먹는다면 기본 구현이 적용되기 때문에 문제가 발생할 수 있다.
Derived class에서 요청하는 경우에만 기본 구현이 적용되도록 하려면, 가상 함수의 인터페이스와 기본 구현을 분리하면된다.
순수 가상 함수를 만들고, 그에 대한 기본 구현을 non-virtual 함수로 제공한다.
그러면 기본 구현을 사용하고 싶은 클래스는 가상 함수 내부에서 non-virtual 함수를 호출해야 한다.
이때, non-virtual 함수는 protected로 선언되는 것이 좋다.
순수 가상 함수의 구현을 외부에서 정의하는 기능을 사용할 수도 있다.
순수 가상 함수에 대해 구현을 했지만, 순수 가상 함수이기 때문에 모든 derived class에서 재정의를 해야 하므로 프로그래머의 실수를 막을 수 있다.
3. non-virtual function
non-virtual function을 선언하는 목적은 derived class가 함수의 인터페이스 뿐만 아니라, 필수 구현을 상속하도록 하기 위함이다.
비가상 함수는 specialization에 대한 불변을 식별한다.
즉, derived class에서 재정의 되지 않는다.
pure virtual, simple virtual, non-virtual funciton에 대한 차이는 derived class가 상속하기를 원하는 것을 정확하게 지정한다.
: interface only, interface and a default implementation, or interface and a mandatory implementation
이 함수에 대한 선언들은 다른 것을 의미하기 때문에, 함수를 선언할 때 주의해서 선택해야 한다.
피해야 할 흔한 실수가 있다.
1. 모든 함수를 non-virtual로 선언하는 것
derived class에서 specialization을 할 여지를 제공하지 않는다.
non-virtual destructor는 특별히 문제가 될 뿐만 아니라, derived class는 보통 base class처럼 사용되도록 의도되지 않는다.
2. 모든 함수를 vitual로 선언하는 것
만약 클래스 상속과 상관없는 불변 동작을 가지고 있어야 한다면, 비 가상함수를 사용하는 것이 좋다.
분명히 파생 클래스에서 재정의 되면 안되는 함수가 존재할 것이다.
정리하자면, 인터페이스 상속과 구현 상속은 다르다.
public 상속에서는 derived class는 항상 base class의 인터페이스를 상속해야 한다.
pure virtual function은 오직 인터페이스의 상속만 허용한다.
Simple virtual function은 default implementation의 상속과 인터페이스의 상속이 가능하도록 지정한다.
Non-virtual function은 인터페이스의 상속과 필수적인 구현의 상속이 가능하도록 지정한다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] Chapter 7: Templates and Generic Programming(1) (0) | 2022.07.09 |
---|---|
[Effective C++] Chapter 6: Inheritance and Object-Oriented Design(2) (0) | 2022.07.08 |
[Effective C++] Chapter 5: Implementations(2) (0) | 2022.07.06 |
[Effective C++] Chapter 5: Implementations(1) (0) | 2022.07.05 |
[Effective C++] Chapter 4: Design and Declarations(2) (0) | 2022.07.05 |