- 좋은 인터페이스는 제대로 쓰기엔 쉽고 틀리게 쓰기엔 어렵다. 인터페이스를 만들 때는 이 특성을 지닐 수 있도록 고민해야 한다.
- 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산 제한, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기가 있다.
- 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 기본 제공 타입과의 동작 호환성 유지하기가 있다.
- tr1::shared_ptr은 사용자 정의 삭제자를 지원한다. 이 특징 때문에 tr1::shared_ptr은 교차 DLL 문제를 막아 주며, 뮤텍스 등을 자동으로 잠금 해제하는 데 사용할 수 있다.
인터페이스는 제대로 쓰기엔 쉽게, 틀리게 쓰기엔 어렵게
C++에서 인터페이스의 활용도는 매우 높다.
인터페이스 설계에 중요한 포인트는 제대로 쓰기엔 쉽고 틀리게 쓰기엔 어려워야 한다는 것이다.
그러려면 우선 사용자가 저지를 만한 실수를 방지해야 한다.
예를 들어, 날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하고 있다고 가정하자.
class Date {
public:
Date(int month, int day, int year);
...
};
문제가 없어 보인다. 하지만 사용자가 실수할 수 있는 부분이 몇 가지 있다.
- 매개변수의 전달 순서가 잘못될 수 있다.
- 유효하지 않는 날짜를 전달할 수 있다.
사용자의 실수를 방지하는 방법은 여러 가지가 있다.
- 새로운 타입 만들기
- 타입에 대한 연산 제한
- 객체의 값에 제약
- 자원 관리 작업을 사용자에게 맡기지 않기
새로운 타입 만들기
새로운 타입을 통해 인터페이스를 강화하면 사용자의 실수를 막을 수 있다.
wrapper 타입을 각각 만들고 이 타입을 Date 생성자 안에 둘 수 있다.
struct Day{
explicit Day(int d) : val(d) {}
int val;
};
struct Month{
explicit Month(int m) : val(m) {}
int val;
};
struct Year{
explicit Year(int y) : val(y) {}
int val;
};
class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30, 3, 1995); // error, 타입이 틀림
Date d(Day(30), Month(3), Year(1995)); // error, 타입이 틀립
Date d(Month(3), Day(30), Year(1995)); // OK
Day, Month, Year에 적절히 기능을 하는 온전한 클래스면 더 좋겠지만, 타입을 준비해 두기만 해도 에러를 방지할 수 있다.
그리고 타입에 적절한 제약을 사용할 수있다. 예를 들어 월이 유효한 값은 1 ~ 12이다.
enum을 사용할 수도 있다. 안전성은 떨어지지만 편의성이 좋다.
Class Month {
public:
static Month Jan() { return Month(1); }
static Month Feb() { return Month(2); }
...
private:
explicit Month(int m);
...
};
Date d(Month::Mar(), Day(30), Year(1995));
특정한 월을 나타내는 데 객체를 쓰지 않고 함수를 쓴 것은 비지역 정적 객체들의 초기화를 믿고 사용하는 것은 문제가 되기 때문이다.
타입에 제약 부여
어떤 타입에 제약을 부여하여 그 타입을 통해 할 수 있는 일들을 제한하는 방법이 있다.
흔히 쓰이는 예가 const를 붙이는 것이다.
operator*의 반환값에 const를 붙여 결과를 변경할 수 없게 만드는 예시이다.
if ( a * b = c ) ... // error
그리고 여기에는 한가지 팁이 있다.
그렇게 하지 않을 이유가 없다면 사용자 정의 타입은 기본제공 타입처럼 동작하게 만들어라.
a와 b가 int라면 a*b에 대입을 한다는 게 말이 안 된다. 그러니 여기서도 안되게 막아야 한다.
기본제공 타입과 어긋나는 동작을 피하는 실질적인 이유는 일관성 있는 인터페이스를 제공하기 위해서이다.
일관성은 인터페이스 설계에 있어 중요한 부분이니 신경써야 한다.
STL 컨테이너의 인터페이스는 전반적으로 일관성을 갖고 있어 사용하는 데 부담이 없다.
예시로는 모든 STL 컨테이너는 size란 멤버 함수를 열어 놓았다.
그 반대의 경우에는 자바의 배열에는 length 프로퍼티를 사용하고, String은 length 메서드를 불러야 하며 List에 대해서는 size 메서드를 사용하도록 되어 있다.
닷넷은 Array에서 프로퍼티 이름으로 length를 사용하는데 ArrayList에서는 원소 개수를 세는 프로퍼티의 이름은 Count이다. 이를 다 외우지 않는 이상 많이 혼란스러울 수 있다.
자원 관리 작업을 사용자에게 맡기지 않기
자원 관리 객체를 만들어 자원 누출을 막는 방법은 매우 좋은 방법이다.
하지만 사용자가 인지하고 사용하는 경우에는 문제가 없지만 이를 까먹고 사용하지 않으면 문제가 발생할 수 있다.
그러니 애초에 tr1::shared_ptr 같은 자원 객체 관리를 사용자에게 주는 팩토리 함수를 만들면 해결할 수 있다.
std::tr1::shared_ptr<Investment> createInvestment();
이러면 사용자는 부담없이 createInvestment 함수만 사용해도 문제가 없다.
tr1::shared_ptr을 반환하는 방법은 자원 해제와 관련된 사용자 실수를 방지할 수 있는 좋은 방법이다.
그리고 tr1::shared_ptr은 삭제자를 지정할 수 있어 이를 활용하면 원하는 대로 소멸자를 처리할 수 있다.
//getRidOfInvestmentd라는 함수가 있다고 가정
std::tr1::shared_ptr<Investment> craeteInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestmentd); // getRidOfInvestmentd라는 함수를 소멸자로 가지며
// null 포인터를 갖는 임시 return 값
retVal = ...; // 실제 객체를 가리키도록
return retVal;
}
첫 번째 인자는 스마트 포인터를 관리할 실제 포인터이고 두 번째 인자는 참조 카운트가 0이 될 때 호출될 삭제자이다.
retVal로 관리할 실제 객체의 포인터를 결정하는 시점이 retVal을 생성하는 시점보다 앞설 수 있으면 retVal을 null로 초기화하고 나중에 대입하는 방법보다 실제 객체의 포인터를 바로 retVal의 생성자에 넘겨주는 것이 더 좋다.
또한, tr1::shared_ptr은 생성한 DLL과 동일한 DLL에서 delete를 사용하도록 만들어져 있기 때문에 교차 DLL 문제를 해결할 수 있다. (교차 DLL: 객체 생성 시 new/delete의 동적 링크 라이브러리 짝이 안맞는 경우)