2016년 3월 20일 일요일

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

*여기서 인터페이스는 c++에서쓰이는 함수, 클래스, 템플릿 등을 모두 포괄하는 의미이다.

 사용자가 저지를 수 있는 오류에는 여러 가지가 있고, 개발자는 그러한 것들을 최대한 줄일수 있도록 여러 안전 장치들을 마련해 놓을 필요가 있다.

Ex1) 타입 정의하기.
Class Date{
Public:
   Date(int month, int day, int year);
   ……
};
오류1. 매개변수의 전달 순서가 잘못될 여지가 있다.
Date d(303, 1996);     //  3, 30이어야 하는데…”30, 3을 넣었음.

오류2. 월 과 일에 해당하는 숫자가 어이없는 숫자일수 있다.
Date d(3, 40, 1995);          //  40은 날짜의 범위를 초과함.

à이런 사소한 인터페이스 사용 에러를 사용자에게 사전에 인식시키기 위해서 적절한 타입을 설정해 놓을 수 있다.

struct day
{                  
struct month
{                   
struct year
{
explicit day(int d) : val(d)
{


explicit month(int m):val(m)
{

}
explicit year(int y)
{

}
int val;
int val;
int val;
};
};
};

Calss Date{
Public:
Date(const Month& m, const Day& d, const year& y);
..
};
Date d(30, 3, 1995);                                               //에러, 타입이 틀렸습니다.
Date d(Day(30, Month(3), Year(1995));                  //에러, 타입이 틀렸습니다.
Date d(Month(3), Day(30), Year(1995));                 //ok, 타입이 전부 맞았습니다.

à이것으로도 적절한 타입에 대한 예는 충분히 보일 수 있지만…….
Class Month{
Public:
   Static Month Jan() {return Month(1);}
   Static Month Feb() {return Month(2);}
   ..
   Static Month Dec() {return Month(12);}
  ……
Private:
   Explicit Month(int m);             //month 값이 새로 생성되지 않도록 명시호출 생성자가
.                                              //private멤버이다.  월표현을 위한 내부 데이터.
};

단순한 구조체에서 각 타입의 값에 제약을 가하는 등의 추가적인 여러 작업을 할 수 있는 온전한 클래스는 만드는 것도 좋다.


Ex2) 제약 부여하기 & 기본 자료형과 동작 일치 시키기
if(a * b = c) .  //본래 비교하려는 의도였음.
à operator*의 반환 타입을 const로 한정함으로써 이러한 실수를 방지함
à 위의 코드에서 a ,b c 가 기본자료형(int 등)이라면 컴파일 에러가 나겠지만, 사용자 정의 자료형은 operator*의 반환형이 상수형이 아닐경우 복사 연산이 이루어질수 있다. 이러한 동작은 사용자가 생각하기에 비상식적인 동작 (기본자료형과 다르게 동작하므로)이므로 특별한 이유가 없는 이상 int 등의 동작 원리와 맞추는 것이 좋다.

Ex3) 자원 관리 작업의 책임을 사용자에게 넘기지 말자.
Investment* createInvestment();        //항목 13에 있는 함수.
 Investment 클래스 계통에 속해 있는 어떤 객체를 동적 할당하고 그 객체의 포인터를 반환하는 함수.

à 이 함수를 사용한 후에는 자원 누출을 피하기 위해 createInvestment에서 얻어낸 포인터를 삭제해야한다. 항목 13에서는 여기서 얻은 포인터를 tr1::shared_ptr등의 스마트 포인터에 저장한 후에 해당 포인터의 삭제 작업을 스마트 포인터에세 떠넘기는 방법을 사용했다. 하지만 이 스마트 포인터를 사용하는 것 역시 사용자가 해야한다.


Std::tr1::shared_ptr<Investment> createInvestment();
à 애초부터 팩토리 함수가 스마트 포인터를 반환하게 만들었다. 이렇게 해 두면, 이 함수의 반환값은 tr1::shared_ptr 에 넣을 수 밖에 없을 뿐더러, 나중에 객체를 삭제하는 것을 깜빡할 일도 없다.


Ex4) 사용자 정의 삭제자를 가진 널 shared_ptr을 생성하는 방법

-tr1::shared_ptr은 생성 시점에 자원 해제 함수(일명 삭제자)를 직접 지정할 수도 있다.
Std::tr1::shared_ptr<Investment>pInv(static_cast<Investment*>(0), getRidOfInvestment);

- createInvestment함수에서 getRidOfInvestment를 삭제자로 갖는 tr1::shared_ptr을 반환하는 방법

Std::tr1::shared_ptr<Investment> createInvestment(0
{
    Std::tr1::shared_ptr<Investment>retVal(static_cast<Investment*>(0), getRidOfInvestment);
   
    retVal = ;                              // retVal은 실제 객체를 가리키도록 만든다.
    Return retVal;
}
à retVal을 위처럼 널로 초기화하고 나서 실제 객체의 포인터를 대입하는 것보다 실제 객체의 포인터를 바로 retVal의 생성자에게 넘겨버리는게 더 낫다. (항목 26참조)

@ tr1::shared_ptr의 또 하나의 특징 : 교차 DLL 문제(cross-DLL problem) 방지
à 객체 생서 시에 어떤 동적 링크 라이브러리(DLL)의 new를 썼는데 그 객체를 삭제할 떄는 이전의  DLL과 다른 DLL에 있는 delete를 썼을 경우, new/delete 짝이 실행되는 DLL 이 다르면 런타임 에러가 발생하게 된다. 하지만 tr1::shared_ptr의 기본 삭제자는 tr1::shared_ptr이 생성된 DLL과 동일한 DLL에서 delete를 사용하도록 만들어져 있다.





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



댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...