항목 18. 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
1. 사용자 타입시스템을 활용하여 인터페이스를 구성해보자.
Class Date {
public :
Date(int month, int day, int year);
…
};
1) 풀이 - 날짜 클래스의 생성자 예시이다.
2) issue - 매개변수 순서가 month, day year일때 사용자가 실수로 다른값을 넣을 수 있다.
3) 해결방법 - 새로운 타입을 들여와 인터페이스를 강화하여 해결하자. (아래 예시 참조)
ex) 3) 예시
struct Day{
explicit Day(int d) : val(d) {}
int val;
}
struct Month{
explicit …
}
struct Year{
explicit …
}
//이렇게 사용가능하다.
Date d(Month(3), Day(14), Year(2020)); // 타입 일치
(가) 풀이 - 일 원 연을 구분하는 랩퍼 타입을 struct로 만들어 이타입을 date 생성자안에 넣자. 이정도로도 일단 충분하다.
(나) 참고사항 - year에 enum을 사용할 수 있지만 안전성은 좋지 않다. 타입 안전성이 신경쓰인다면 쓰지마라.
(다) 바로 위 예제는 비지역 정적 함수를 사용한다. 왜 비지역 정적객체는 사용하면 안될까?
ㄱ) 빌드시 초기화 순서문제때문이다. 4장에 있다.
2. const를 활용한 인터페이스
1) const를 활용하여 제약하자. 아래 예제는 operator 관련 학습때 진행했던 예제이다.
if(a*b = c) // 비교를 원했지, 대입을 하려던게 아니다.
2) int type 처럼 사용자타입도 동일하게 동작하도록 하는 코드를 만들어라.
3. 일관성있는 인터페이스는 제대로 쓰기에 괜찮은 인터페이스를 만들어주는 가장 큰 요인이다.
4. 사용자쪽에서 외워야 하는 인터페이스는 잘못쓰기 쉽다.
ex) 팩토리 함수
Investment* createInvestment();
(1) 풀이 - 위 코드는 해당 함수를 통해 얻어낸 포인터를 삭제해야한다.
1) 실수 1 - 포인터 삭제를 까먹음
2) 실수 2 - 포인터를 두번이상 삭제함
(2) 해결방법 - auto_ptr과 같은 스마트포인터에 저장한 후 사용
1) 실수 1 - 스마트포인터를 사용하는 걸 까먹었다면?
(가) 해결방법 - 설계 시 스마트포인터를 반환하게한다.
std::tr1::shared_ptr<Investment> createInvestment();
(나) 풀이 - 애초에 팩토리 함수가 스마트포인터를 반환하게 한다.
5. tr1::shared_ptr을 살펴보자.
- shared_ptr은 두개의 인자를 받는 생성자가 있다.
(1) 실제 포인터
(2) 참조카운트가 0일때 호출되는 삭제자.
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
retVal = ...;
return retVal;
(3) 풀이 - 위 생성자를 보면 첫번째 매개변수로 포인터를 널로초기화하고, 나중에 대입하는 방식으로 구성되어있다.
(4) 더좋은 방법 - 실체 객체 포인터를 바로 생성자에 넘기는 방법이다. (항목 26에서 공부해보자)
6. tr1::shared_ptr의 장점
(1) shared ptr의 좋은점은 포인터별(per pointer) 삭제자를 자동으로 씀 -> 교차 DLL문제를 해소함
(2) 교차 DLL이란?
(가) 1번 dll의 new를 썻는데 2번 dll의 delete를 사용햇을때 꼬인다. -> 런타임 에러
(3) 하지만 shared_ptr은 동일한 dll에서 new ,delete를 사용한다.
7. boost 라이브러리
(1) shareed_ptr을 잘구현한 것은 boost라이브러리이다.
(2) 단점1 - 부스트의 shared_ptr은 일반 ptr크기의 2배이다.
(3) 단점2 - 또한, 삭제자메커니즘, 내부관리데이터를 사용할때 동적메모리 사용
(4) 단점3 - 다중스레드 동기화 오버헤드를일으킨다.
(5) 정리 - 이 클래스를 쓰면 원시포인터보다 크고 느리며 동적메모리까지 추가로 물린다.
하지만, 이런것들땜에 런타임 비용이 크게 늘어나는경우는 거의없다.
이것만은 잊지말자!
(1) 좋은인터페이스는 제대로 쓰기에 쉬우며, 엉터리로 쓰기에 어렵게 하자. 고민하자. 어떻게 설계할지
(2) 인터페이스의 올바른 사용을 이끄는 방법은 인터페이스 사이의 일관성 잡아주기, 기본타입과의 호환성유지
(3) 사용자의 실수를 예방하기위해, 새로운 타입 만들기, 타입에 대한 연산제한, 객체의 값에 대한 제약걸기, 자원 관리작업을 사용자 책임으로 두지 않기
(4) tr1::shared_ptr은 사용자 정의 삭제자를 지원한다. 이때문에 교차 dll 문제를 막아주며, 뮤텍스등을 자동으로 잠금 해제하는데쓸수 있다.
참고자료 : 정적라이브러리, 동적라이브러리 (https://kali-km.tistory.com/entry/DLL%EC%9D%B4%EB%9E%80)