본문 바로가기
Effective C++

이펙티브(Effective) C++ item 34 : 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자

by gong재이 2022. 6. 26.
반응형

상속에는 2가지 종류가 있다.

  • 인터페이스 상속
  • 구현 상속

설계 상황에 따라 인터페이스 상속만 받을지, 인터페이스와 구현에 대한 상속을 모두 다를지 선택해야 하는 경우들이 생기기 때문에 이 차이를 명확히 하는 것이 중요하다.

 

class Shape{
public:
    virtual void draw() const = 0;
    virtual void error(const std::string& msg);
    int objectID() const;
    ...
};

class Rectangle: public Shape{...};
class Ellipse: public Shape{...};
멤버함수 인터페이스는 항상 상속되게 되어 있다.

함수 형태에 따른 상속 차이

1. 순수 가상함수 : 인터페이스 상속

파생 클래스에게 함수의 인터페이스만을 물려주기 위함이다.

즉, 나는 인터페이스만 줄테니 나머지 구현내용은 파생클래스에서 모두 알아서 하도록 자율성을 준다.

draw() 함수는 도형마다 어떻게 그려야하는지 다른 알고리즘이 나오기 때문에 순수 가상함수로 전달하는 것이 적절하다.

 

2. 단순 가상함수 : 인터페이스 + 구현 상속

함수의 인터페이스 뿐만 아니라 그 함수의 기본 구현도 물려받게 하자.

 

단순 가상함수를 상속받는 경우는 파생클래스에게 "내가 기본적인 동작 원리는 구현해줄게. 너희가 추가로 구현할 부분이 있으면 추가 구현해. 그런게 없으면 내가 알려준 기본 동작을 쓰면 돼." 의 느낌이다.

error() 함수도 따로 에러처리를 할 부분이 없으면 기본 클래스에 선언되어 있는 에러 방식을 이용하게 된다.

 

단순 가상함수의 경우 인터페이스와 구현부가 모두 상속이 되기 때문에 위험성을 가질 수 있다.

class Airport{...};

class Airplane{
public:
    virtual void fly(const Airport& destination);
    ...
};

void Airplane::fly(const Airport& destination)
{
    // 주어진 목적지로 비행기를 날려보내는 기본 동작 원리를 가진 코드.
}

class ModelA: public Airplane{ ... };
class ModelB: public Airplane{ ... };
class ModelC: public Airplane{ ... };	// fly 선언되지 않음.

위 코드에서 ModelC 는 fly() 함수를 정의하지 않았다고 해보자.

모델마다 fly하는 방식이 다를텐데 기본적인 fly() 동작을 하게 된다면 문제를 이르킬 가능성이 높다.

따라서, 설계상으로 이러한 위험성을 줄이도록 바꿔줄 수가 있다.

 

1. 순수 가상함수 + 비가상 함수

class Airplane{
public:
    virtual void fly(const Airport& destination) = 0;
    ...
protected:
    void defaultFly(const Airport& destination);
};

void Airplane::defaultFly(const Airport& destination)
{
    // 주어진 목적지로 비행기를 날려 보내는 기본 동작 원리를 가진 코드
}

fly() 함수를 순수 가상함수로 만들고 기본 동작을 수행하는 함수 defaultFly() 를 따로 선언하는 것이다.

ModelC은 fly() 인터페이스만 상속받고 구체적 동작은 상속받지 않기 때문에 fly() 를 정의하지 않으면 에러가 발생될 것이다.

만약 특별한 동작이 없다면 defaultFly() 함수만 호출하면 되고, model에 맞게 추가 구성도 할 수 있다.

 

defaultFly는 비가상함수로 선언되어 있는데, 단순 가상함수로 선언하면 위에서 발생되었던 문제를 다시 겪을 것이기 때문이다.

 

2. 순수 가상함수의 구현

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)
    { Airplane::fly(destination);}
}

defaultFly 대신에 순수가상함수에 본문이 들어와있는 구조이다.
C++에서는 순수가상함수에도 정의를 제공할 수 있다. 다만 구현이 붙은 순수 가상 함수를 호출하려면 반드시 클래스 이름을 한정자로 붙여 주어야만 한다. 왜냐하면 추상 클래스로는 인스턴스를 만들 수 없기 때문이다.

 

3. 비가상함수 : 인터페이스 + 구현 상속

파생클래스가 함수 인터페이스와 더불어 그 함수의 필수적인 구현(mendatory implementation)을 물려받게 한다.

비가상멤버 함수는 클래스 파생에 상관없이 변하지 않는 동작을 지정하는데 이용된다.

이는 맨 처음 설계할 때 파생클래스에서도 항상 똑같은 동작(불변동작)이 일어나야 하는 것을 전제하고 있다.

 

클래스 설계시 자주하는 실수 2가지

1. 모든 멤버 함수를 비가상 함수로 선언

파생클래스를 만드는 목적이 없어지게 된다.
기본클래스 동작을 특별하게 만들만한 여지를 없애고, 특히 비가상 소멸자가 문제거리가 될 수 있다.

기본 클래스로 쓰일 수 있는 함수는 대부분 가상함수를 가지고 있게 된다.

 

2. 모든 멤버 함수를 가상함수로 선언

인터페이스 클래스의 경우 이렇게 만들 수 있다.

하지만 분명히 파생 클래스에서 재정의가 안되어야 하는 함수도 있기 때문에 이런 함수가 있으면 비가상함수로 만들어 두어야 한다.

반응형

댓글