[Effective C++] 18. 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

C++/Effective C++

2020. 8. 6. 21:18

1. 실수를 할 수 있는 코드의 예

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


Date d(30,3,1995) // month와 day를 바꿔 넣음
Date d(3,40,1995) // day가 될 수 없는 값이 들어감

이를 방지하기 위해서 Month , Day , Year을 따로 구조체로 빼는 방법과

클래스를 이용해 비지역 정적 멤버 함수들로 값을 반환시킬 수 있다.

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

class Date{
public:
     Date(const Month& m, const Day& d, const Year& y);
};

// or 정적 지역 멤버 함수
class Month{
public:
     static Month Jan() {return Month(1);}
     static Month Feb() {return Month(2);}
     // ...
private:
	 explicit Month(int m);
};

 

Jan,Feb들을 객체화 시키지 않고 함수로 제공하는 이유는 비지역 정적 객체의 초기화의 순서를 보장할 수 없기 때문!

https://sanghun219.tistory.com/139

 

[Effectivce C++] 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자.

1. 초기화 규칙 C++ 객체 초기화에 대한 규칙은 명확히 준비되어 있으나 복잡하여 그것을 외우기에 가치가 떨어진다. 따라서, 가장 추천하는 방법은 모든 객체를 사용하기 전에 항상 초기화하는 �

sanghun219.tistory.com

여기서 자세히 설명하고 있다.

 

2. 그렇게 하지 않을 번듯한 이유가 없다면 사용자 정의 타입은 기본제공 타입처럼 동작하게 하자

말이 길다.. 에러 코드를 보면서 설명해보겠다.

 

if ( a * b = c) ...

 

위의 문법은 굉장히 이상하다.

원래의 문장은  ( a * b == c) 즉, 비교를 하려던 문장인데 사용자의 실수로 잘못 사용된 것.

위를 방지하기 위해서 const를 이용하면(객체 operator부분에) 컴파일 에러가 뜰 것이다.

따라서 소제목의 뜻은 우리가 임의로 만든 객체가 우리가 당연하게 생각하는 방식으로 돌게 하려면

예외상황을 사전에 방지할 수 있도록 코드를 짜라는 뜻이다.

 

3. 사용자쪽에서 외워야 제대로 쓸 수 있는 인터페이스

개인적으로 c#을 하고 있어서 그런지 책의 예제가 와닿았다. c#에서는 Array에는 length가, ArrayList에는 count가 존재한다. 이는 각각 원소의 개수를 반환하는 프로퍼티인데(멤버변수정도로 생각) 가끔가다가 헷갈린다. 같은 의미이지만 혹여나 배열의 끝을 말하는지 원소의 개수를 말하는지.. 이름이 다르기에 한 번 더 생각을 해야 했다.

 

이와 마찬가지로 책에서는 c++의 포인터와 스마트 포인터를 예로 들고 있다.

Investment* createInvestment();

위의 함수는 사용자가 실수를 일으킬 요소를 두 가지나 가지고 있다. 이 함수는 전적으로 사용자가 메모리 관리를 하게끔 만들어졌다. 즉, 할당은 내가 할테니 해제는 네가 하라식.. delete를 잊는 경우와(혹은 예외로 인해 해제하지 못한경우) 똑같은 포인터에 대해 delete가 두 번 이상 적용될 가능성도 존재한다.

 

이를 방지하기 위해 스마트 포인터를 도입했다. 하지만, 위의 함수를 스마트 포인터로 받아야 한다는 것은 어폐가 있다. 왜냐하면, 위의 소제목이 말하듯이 그건 쓸데없이 사용자에게 사용법을 강제로 암기시키는 것이기 때문. 따라서, 이를 방지하기 위해 애초에 createInvestment()가 스마트 포인터를 반환하는 방식으로 설계하는 것이다.

std::shared_ptr<Investment> createInvestment();

 

 

이렇게 해두면, 사용자는 반드시 스마트 포인터로 받아야하기에 자원 누출을 강제로 피할 수 있으며 따로 외워서 쓸 필요도 없는 구조가 됐다.

 

4. shared_ptr

shared_ptr은 인자로 두 개를 받을 수 있다. 하나는 실제 포인터이고, 나머지 하나는 삭제자(deleter)이다. 삭제자는 shared_ptr이 참조 카운트가 0이 됐을 때 호출 할 함수 객체이며 보통 배열에 대한 shared_ptr을 delete[] 해주기 위해 사용된다. (이렇게 쓰기 싫다면 shared_ptr을 vector로 만들라.)

 

위의 createInvestment() 함수에서 return 될 포인터 관리자보다 실제 포인터가 결정될 시기가 더 늦는 경우도 생각해보자.

std::shared_ptr<Investment> createInvestment(){

std::shared_ptr<Investment> retVal(static_cast<Investment*>(0),getRidOfInvestment());

return retVal;
}

첫 번째 요소로 실제 포인터가 들어가야 하지만 아직 포인터가 결정되지 않은 경우에는 위와같이 Investment 포인터 형식의 0(NULL)을 넣어주도록 하자.

 

shared_ptr을 이용하면 교차 DLL 문제도 해결할 수 있다고 한다. 객체 생성시에 어떤 동적 링크 라이브러리의 new를 썼는데 그 객체를 삭제할 때는 이전의 DLL과 다른 DLL에 있는 delete를 썼을 경우에 런타임 에러가 난다.

shared_ptr을 이용하면 기본 삭제자가 생성된 DLL과 동일한 DLL에서 delete를 사용함으로 런타임 에러를 피할수 있다.

 

이것만은 잊지 말자!

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