KoreanFoodie's Study

Effective C++ | 항목 18 : 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 본문

Tutorials/C++ : Advanced

Effective C++ | 항목 18 : 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

GoldGiver 2022. 10. 25. 16:12

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 18 : 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

핵심 :

1. 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터리로 쓰기에 어렵다. 인터페이스를 만들 때는 이 특성을 지닐 수 있도록 고민하고 또 고민하자
2. 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본제공 타입과의 동작 호환성 유지하기가 있다.
3. 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기가 있다.
4. std::shared_ptr 는 사용자 정의 삭제자를 지원한다. 이 특징 때문에 교차 DLL 문제를 막아주며, 뮤텍스 등을 자동으로 잠금 해제하는 데(항목 14 참조) 쓸 수 있다.

 

다음 클래스는 생각보다 잘못될 여지가 많다!

class Date
{
  public:
    Date(int month, int day, int year);
};

...

// month 와 day 를 거꾸로..
Date d1(30, 3, 1995);
// 쩝, day 에 오타를 냈네요
Date d2(3, 40, 1995);

위의 어이없는(하지만 충분이 있음직한) 실수를 방지하기 위해서, 타입을 새로 만들어주는 것도 좋은 방편이 될 수 있다.

struct Day
{
  explicit Day(int d)
  : val(d) {}
  
  int val;
};

// Month, Year 도 위와 비슷하게...

...

// Date 클래스 재정의
class Date
{
  Date(const Month& m, const Day& d, const Year& y);
  ...
};

// 타입 에러!
Date d1(30, 3, 1995);

// 타입 에러!
Date d2(Day(30), Month(3), Year(1995));

// 정상
Date d3(Month(3), Day(30), Year(1995));

타입 안정성을 조금 더 신경쓴다면, 다음과 같이 만들 수 있다.

class Month
{
  public:
    // 유효한 Month 값을 반환하는 함수들
    static Month Jan() { return Month(1); }
    static Month Feb() { return Month(2); }
    ...
    static Month Dec() { return Month(12); }
    ...
  private:
    // Month 값이 새로 생성되지 않도록 private 으로!
    explicit Month(int m);
};

Date d(Month::Mar(), Day(30), Year(1995));

위에서 월을 나타내는데 객체를 쓰지 않고 함수를 쓴 이유는 비지역 정적 객체들의 초기화 순서는 개별 번역 단위에서 정해지기 때문이다! 항목 4를 떠올리면 된다.

 

다음과 같은 팩토리 함수는 어떤 문제가 있을까?

Investment* createInvestment();

자원 누출을 피하기 위해 createInvestment 에서 얻어낸 포인터는 나중에 삭제해야 한다. 하지만 delete 를 깜빡할 수도 있고, shared_ptr 같은 RAII 객체로 감싸는 것을 잊어버릴 수 있다. 따라서 팩토리 함수 자체가 RAII 객체를 반환하게 만들면, 문제가 해결된다.

std::shared_ptr<Investment> createInvestment();

혹시 getRidOfInvestment 라는 삭제자 역할을 하는 함수를 만들면 어떨까? 다음과 같이 말이다.

std::shared_ptr<Investment> createInvestment()
{
  // 0 은 int 이므로 컴파일이 안됨
  std::shared_ptr<Investment> pInv(0, getRidOfInvestment);
  
  // null shared_ptr 가 생성
  std::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
  
  // revVal 은 실제 객체를 가리키도록 만듦
  retVal = ...;
  
  return retVal;
}

하지만 위처럼 retVal 로 관리할 실제 객체의 포인터를 결정하는 시점이 retVal 을 생성하는 시점보다 앞설 수 있으면, 위보다 실제 객체의 포인터를 바로 retVal 의 생성자에 넘겨버리는 게 더 낫다. 이유는 항목 26에서 후술한다.

 

shared_ptr 의 또 다른 장점은, '교차 DLL 문제(cross-DLL problem)' 을 막아준다는 것이다. 객체 생성 시에 어떤 동적 링크 라이브러리(dynamically linked library: DLL) 의 new 를 썼는데 삭제할 때는 이전의 DLL 과 다른 DLL 에 있는 delete 를 쓰면 문제가 일어난다. 그런데 shared_ptr 는 생성된 DLL 과 동일한 DLL 에서 delete 를 사용하도록 만들어져 있다.

Comments