인터페이스 = 함수, 클래스, 템플릿 …..
이걸 설계 신조로 삼자.
"제대로 쓰기엔 쉽고, 엉터리로 쓰기엔 어렵게"
이걸 설계 신조로 삼자.
예를 들어, 날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하고 있다고 가정하자.
별 문제 없어보인다. 근데 인터페이스 설계 => 개발자의 눈이 아니라 사용자의 눈으로 봐야됨. 사용자가 어떤 실수를 저지를까?
가령 매개변수의 전달 순서가 잘못될 여지가 있다.
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문제를 막아주며, 뮤텍스 등을 자동으로 잠금 해제하는 데 쓸 수도 있다.
ㄴ
'Effective C++ > 4. 설계 및 선언' 카테고리의 다른 글
항목 23: 함수보다는 비멤버 비프렌드 함수와 더 가까워지자 (0) | 2021.04.24 |
---|---|
항목 22: 데이터 맴버가 선언될 곳은 private 영역임을 명심하자. (0) | 2021.04.24 |
항목 21: 함에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자. (0) | 2021.04.24 |
항목 20: '값에 의한 전달'보다는 '상수 객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다. (0) | 2021.04.24 |
항목 19: 클래스 설계는 타입 설계와 똑같이 취급하자. (0) | 2021.04.24 |