본문 바로가기
C++/Effective C++

Effective C++ (31) ~ (35)

by 계발자 망고 2021. 8. 24.

31) 파일 사이의 의존성을 최대로 줄이자

#include는 헤더 파일들 사이에 컴파일 의존성을 만든다.

보통은 전방 선언으로 이러한 문제를 해결한다.

이 때 표준 라이브러리들은 전방 선언을 하지 않는다. precompiled header를 쓰는 환경이라면 더더욱 그럴 필요가 없다.

 

전방 선언시 pimpl 관용구를 통해 인터페이스와 구현을 분리하면, 구현을 수정했을 때 해당 클래스를 사용하는 다른 부분은 컴파일을 다시 할 필요가 없게 된다.

아래 foo 클래스와 같이 pimpl 관용구를 사용하는 클래스를 핸들 클래스라고 한다. 

class fooImpl;

class foo{
private:
  std::shared_ptr<fooImpl> pImpl;
}

컴파일 의존성을 최소화하는 전략은 다음과 같다.

  • 객체 참조자나 포인터로 충분한 경우 객체를 직접 쓰지 않는다
  • 할 수 있으면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 만든다 : 함수의 반환값이나 인자로 전달할 때에는 클래스 정의가 필요하지 않다.
  • 선언부와 정의부에 별도의 헤더 파일을 제공한다 : 두 파일을 별도로 분리하고, 실제로는 클래스를 전방선언 하지 않고 선언부 헤더를 include한다.

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

32) public 상속 모형은 반드시 "is-a"를 따르도록 만들자

public 상속을 했다면 그것은 바로 is-a 를 의미한다. Derived는 Base 클래스이나 그 반대는 성립하지 않는다.

기본 클래스의 모든 속성이 파생 클래스에 적용된다는 뜻이므로, 설계 시 유의하자.

예를 들어 Bird를 상속한 날 수 없는 펭귄이나, Rectangle을 상속한 Square은 문제를 일으킬 소지가 다분하다.

33) 상속된 이름은 숨기지 말자

여기서 이름이란 변수 혹은 함수의 이름을 뜻한다.

파생 클래스의 유효 범위(scope)가 기본 클래스 스코프 내에 중첩되어 있으므로, 이름이 같을 때 문제가 발생한다.

왜냐면 이름 가리기는 함수의 매개변수 타입이 다르거나 말거나 신경쓰지 않기 때문이다. 가상 함수 여부에도 상관없다.

만약 이름 가리기를 무시하고 싶은 경우 using문을 사용할 수 있다. 이때 side effect를 줄이기 위해 다음과 같이 전달 함수를 만들어 활용할 수도 있다.

class Base{
public:
  virtual void mf1() = 0;
  virtual void mf1(int);
};

class Derived : private Base{
public:
  virtual void mf1()
  { Base::mf1(); }
};

34) 인터페이스 상속과 구현 상속의 차이를 구별하자

멤버 함수 인터페이스는 항상 상속된다. 멤버 함수를 상속시키는 형태에는 세가지가 있다.

  • 순수 가상 함수 : 파생 클래스에 함수의 인터페이스만을 물려주게 된다. 파생클래스는 반드시 이 함수의 구현부를 제공해야 한다. ( 사실 순수 가상함수에도 정의를 제공할 수 있기는 하다 )
  • 단순 가상 함수 : 인터페이스 뿐만 아니라 기본 구현을 물려받게 한다. 단, 파생 클래스에서 사용자가 구현을 제공하지 않았을 경우 기본 구현을 강제로 물려받게 되므로 부작용이 발생할 수 있다. 그럴 때 다음과 같이 해결할 수 있다.
    class foo{
    public:
      virtual void bar() = 0;
    protected:
      void defaultBar();
    }​
     파생클래스에서 선택적으로 기본 구현을 사용할지 결정할 수 있게 된다.  
  • 비가상 함수 : 파생 클래스가 인터페이스와 더불어 필수적인 구현을 물려받게 한다. 파생 클래스는 동작을 절대 재정의할 수 없다.

한편 클래스를 설계할 때 하기 쉬운 실수 두가지는 다음과 같다.

  • 모든 멤버를 비가상 함수로 선언한다
  • 모든 멤버를 가상 함수로 선언한다

목적에 맞는 형태로 선언하자.

35) 가상함수 대신 쓸 것에 대해 대비하자

항상 문제를 가상함수로만 해결할 수 있는 것은 아니다.

  • 템플릿 메서드 패턴 : NVI 관용구로 알려져 있다. public 비가상 함수를 구현해 호출부를 열어두고, 해당 함수에서 호출할 private 가상 함수를 만들어 파생 클래스가 이를 재정의할 수 있도록 한다. 상속받은 private 가상 함수를 파생 클래스가 재정의할 수 있다는 사실은 어색하게 느껴질지 몰라도 정상적인 C++ 문법이다.
  • 함수 포인터로 구현한 전략(strategyt) 패턴 : 클래스가 함수 포인터를 가지며 이 함수 포인터 멤버에 원하는 비 멤버 함수를 설정할 수 있다.
  • std::function으로 구현한 전략 패턴 : 위 방법에서 함수 포인터를 std::function으로 대체해 다양한 융통성을 얻어낼 수 있다.
  • 고전적 전략 패턴 : 한쪽 클래스의 가상 함수를 다른 계층의 가상 함수로 대체하는 전통적 패턴이다.

각 방법의 장단점이 있으니, 가상 함수가 아닌 방법으로 문제를 해결하고 싶을 때에 고민해 보자.

 

 

반응형

'C++ > Effective C++' 카테고리의 다른 글

Effective C++ (41) ~ (45)  (0) 2021.08.26
Effective C++ (36) ~ (40)  (0) 2021.08.25
Effective C++ (26) ~ (30)  (0) 2021.08.22
Effective C++ (21) ~ (25)  (0) 2021.08.16
Effective C++ (16) ~ (20)  (0) 2021.08.14

댓글