본문 바로가기

Effective C++/4. 설계 및 선언

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

인터페이스 = 함수, 클래스, 템플릿 ..

 

이걸 설계 신조로 삼자.

"제대로 쓰기엔 쉽고, 엉터리로 쓰기엔 어렵게"

이걸 설계 신조로 삼자.

 

예를 들어, 날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하고 있다고 가정하자.

 

 

문제 없어보인다. 근데 인터페이스 설계 => 개발자의 눈이 아니라 사용자의 눈으로 봐야됨. 사용자가 어떤 실수를 저지를까?

 

가령 매개변수의 전달 순서가 잘못될 여지가 있다.

 

Date d(30,3,1995); <- 3, 30이어야 하는데..

 

또는 오타가 수도 있다.

 

Date d(3,40,1995); <- 30이어야 하는데 40 넣어버림..

 

이럴 매개 변수도 인터페이스화 시켜서(새로운 타입을 들여와 인터페이스를 강화) 강화 해보자. 지금의 경우 일ㅡ월ㅡ연 구분하는 간단한 wrapper type  각각 만들고 타입을 Date 생성자 안에 있다.

 

 

 

 

 

이렇게 타입을 적절히 준비 => 타입의 값에 제약을 걸더라도 괜찮은 경우가 생긴다.

Month 생각해보면 1~12 까지만 오는게 맞다. 한가지 방법으로 enum 있는데 좋은 방법이지만 enum->int 있기 때문에(항목 2 참조) 타입의 안정성이 떨어진다는 단점이 있다,

 

타입 안전성이 신경 쓰인다면, 유효한 Month 집합을 미리 정의해 두어도 괜찮다.

 

 

 

굳이 객체가 아닌 함수로 했나? 이렇게 하면 안되나??

 

 

다른 번역 단위에서 쓰일 경우 문제가 여지가 있다. -> 쓰는게 좋음

 

--------------------------------------------------------------------------------------

 

다른 방법으로는 어떤 타입에 제약을 부여하여, 타입을 통해 있는 일들을 묶어 버리는 방법이다.

??

 

예를 들면 항목 3에서 했던, const 붙이기 이다. operator* 반환 타입에 const라는 제약을 부여하여

 

if(a*b=c) … // 원래 비교할려고 했는데, 오타로 실수할 경우 Error 뜨게 있다.

 

예제는 '제대로 쓰기에 쉽고, 엉터리로 쓰이게 어려운 타입 만들기' 위한 하나의 일반적 지침을 쉽게 알려주기 위해 끄집어 것이다. 이름하여

 

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

 

사용자들이 int 정도는 성질을 알기 때문에, 우리가 사용자들을 위해 만드는 타입도 이와 똑같이 동작하게끔 만들자는 것이다. 위의 문장을 봐도 a,b int라고 생각하면 a*b c assign하는 것은 말이 안된다. 그러니까 굳이 int 다른 길을 걸어갈 이유가 없다면 int 동작원리 그대로 만들자.

 

이렇게 하는 이유(기본제공타입과 쓸데없이 다르게 동작하는것을 피하는 이유) => 일관성 있는 인터페이스

 

가령 STL 컨테이너의 인터페이스는 전반적으로 일관성을 갖고 있어서 사용에 부담이 덜하다. 예를 들어 모든 STL 컨테어너는 size 맴버 함수를 public으로 개방해 놓고 있다. 함수는 어떤 컨테이너에 들어 있는 원소의 개수를 알려준다.

 

 

사용자 쪽에서 뭔가를 외워야 제대로 있는 인터페이스는 잘못 쓰기 쉽다. 언제라도 잊어버릴 있기 때문이다. 예를 들어 항목 13 팩토리 함수를 보면

 

Investment* createInvestment();

 

함수를 사용  때는, 자원 누출을 피하기 위해 createInvenstment에서 얻어낸 포인터를 나중에라도 삭제해야 한다. => 사용자가 포인터 삭제를 깜빡할 있고, 똑같은 포인터에 대해 delete 이상 적용될 있다.

 

그래서 항목 13에서는 createInvestment 반환 값을 스마트 포인터에 저장한 해당 포인터의 delete 작업을 스마트 포인터에게 떠넘기는 방법을 사용한다. => 사용자가 스마트 포인터를 사용하는 방법도 까먹으면?

 

=> 처음부터 팩토리 함수가 스마트 포인터를 반환하도록 하면 된다.

 

std::tr1::shared_ptr<Investment> createInvestment();

 

----------------------------------------------------------------------------------------------------

 

이런 가정도 해보자. createInvestment 통해 얻은 Investment* 포인터를 직접 삭제하지 않게 하고 getRidOfInvestment() 통해 삭제하게 하면 안되나?

 

=> 자원 해제 메커니즘을 잘못 사용하게 있다. getRidOfInvestment() 함수를 까먹고 delete를 한다던가, …

=> createInvestment() 고쳐서 getRidOfInvestment() 삭제자로 묶인 스마트 포인터를 반환하도록 하면 저걸 잘못 사용하고 자시고 것이 없다.

 

구현 해보면 다음과 같이 있다.

 

:: 항목 13. 자원 관리에는 객체가 그만! (shared_ptr, auto_ptr) (tistory.com)

 

std::shared_ptr - cppreference.com

cppreference constructor 예제를 보면 deleter 만드는 정석 방법이 있다.

그리 비맴버 함수 std::make_shared 통해 쉽게 특정 객체를 가르키는 스마트 포인터 인스턴스를 만들 있다. (인스턴스 = 변수 = 메모리공간)

 

 

 

template< class Y, class Deleter >

shared_ptr( Y* ptr, Deleter d );

 

출처: <https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr>

 

 

 

shard_ptr 다른 좋은 : 포인터 삭제자를 자동으로 씀으로써 교차 DLL 문제(cross DLL Problem) 해결해준다.

 

교차DLL문제 : new a.dll에서 했는데 delete b.dll에서 -> new/delete 짝이 실행되는 dll 다를 발생 ==> 런타임 에러

 

하지만 shared_ptr 기본 삭제자는 shared_ptr 생성된 DLL 동일한 DLL delete 사용하도록 만들어져 있기 때문에 교차DLL문제로부터 자유롭다.

 

예를 들어

 

std::shared_ptr<Investment> createInvestment(void) {

std::tr1::shared_ptr<Investment> retVal(new Investment)

return retVal;

}

 

함수가 반환하는 shared_ptr 다른 DLL 사이에 이리 저리 넘겨지더라도 교차DLL 문제를 걱정하지 않아도 된다. Investment 객체를 가르키는 shared_ptr Investment 객체의 참조 카운트가 0 무조건 new 실행한 DLL deleter 사용한다.

 

 

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