[Effective C++] 항목 18 ~ 20
항목 18 : 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
- 사용자가 저지를만한 실수의 종류를 머리에 넣어두고 있어야 한다.
- ex) 날짜를 나타내는 클래스의 생성자
Date(int month, int day, int year);
- 문제점 1) 전달 순서가 잘못될 여지가 열려있다. »- 새로운 타입을 들여와 인터페이스 강화
- 문제점 2) 월과 일에 해당하는 숫자가 어이없는 숫자일 수 있다. »- 적절한 타입이 준비되어 있으면 각 타입값에 제약을 가할 수 있음.
- ex) 날짜를 나타내는 클래스의 생성자
- enum : 타입 안정성이 그리 믿음직하지 못함.(때로는 int처럼 쓰일 수 있다.)
- 어떤 타입이 제약을 부여하여 그 타입을 통해 할 수 있는 일들을 묶어버리자.
- ex) const 붙이기
- 그렇게 하지 않을 번듯한 이유가 없다면 사용자 정의 타입은 기본 제공 타입처럼 동작하게 만들어라. -> 일관성 있는 인터페이스 제공
- 사용자 쪽에서 뭔가를 외워야 쓸 수 있는 인터페이스는 잘못 쓰기 쉽다.
- ex) Investment* createInvestment(); »- delete 사용해야 함을 기억(혹은 스마트포인터를 써야 함을 기억)
- 해결 : 스마트포인터를 반환하게 만든다.
std::shared_ptr<investment> createInvestment();
- shared_ptr이 널 포인터를 물게 함과 동시에 삭제자를 갖게 하는 방법
+
std::shared_ptr<Investment> pInv(static_cast<Investment*>(0), getRidOfInvestment);
- 캐스트하며 만들면 된다.
- shared_ptr은 사용자 정의 삭제자를 지원한다. 이 특징으로 인해 교차 DLL 문제를 막아주며, 뮤텍스 등을 자동으로 잠금해제(14)하는 데 쓰일 수 있다.
항목 19 : 클래스 설계는 타입 설계와 똑같이 취급하자
- 좋은 타입이란?
- 문법(syntax)이 자연스럽다.
- 의미 구조(semantics)가 직관적이다.
- 효율적인 구현이 한 가지 이상 가능하다.
- 클래스 설계 이슈
- 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?
- 클래스 생성자, 소멸자 설계 / 메모리 할당 함수(new, new[], delete, delete[])설계에 영향을 미친다.
- 객체 초기화는 객체 대입과 어떻게 달라야 하는가?
- 초기화와 대입은 해당하는 함수 호출이 아예 다르다.(4)
- 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?
- 어떤 타입에 대해 값에 의한 전달을 구현하는 쪽은 복사 생성자!!!!
- 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?
- 클래스의 불변 속성(클래스 데이터 멤버의 몇 가지 조합 값은 반드시 유효해야 한다)은 클래스 차원에서 지켜줘야 한다.
- 불변 속성에 따라 클래스 멤버 함수 내 에러 점검 루틴이 좌우된다.(특히 생성자, 대입 연산자, 세터 함수)
- 예외나 예외 지정에도 영향을 끼친다.
- 기존의 클래스 상속 계승망(inheritance graph)에 맞출 것인가?
- 상속 받으면 설계는 이들 클래스에 제약받음(특히 멤버 함수 가상 여부)
- 어떤 종류의 타입 변환을 허용할 것인가?
- 암시적(implicitly) 변환 : 타입 변환 함수(operator 반환형), 비명시적 생성자
- 명시적(explicitly) 변환 : 해당 변환을 맡는 별도 이름의 함수를 만듦
- 어떤 연산자와 함수를 두어야 의미가 있을까?
- 멤버 함수 or 그렇지 않은 것을 결정(23, 24, 46)
- 표준 함수들 중 어떤 것을 허용하지 말 것인가?
- private로 선언해야 하는 함수(6)
- 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가?
- ‘public, protected, private, friend’클래스, 함수 결정
- 클래스 중첩 결정
- 선언되지 않은 인터페이스로 무엇을 둘 것인가?
- 만들 타입이 제공할 보장(수행성능, 예외안정성(29), 자원 사용(잠금/동적메모리)이 어떤 종류일까.
- 보장은 클래스 구현의 제약으로 작동
- 새로 만드는 타입이 얼마나 일반적인가
- 정의하는 것이 동일 계열의 타입군(family of types)일 경우 새로운 클래스보다 새로운 클래스 탬플릿을 정의해야 한다.
- 정말로 필요한 타입인가?
- 기능 몇개가 아쉬워 파생 클래스를 만드는 것보단 간단한 비멤버 함수, 템플릿을 정의하자.
- 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?
항목 20 : ‘값에 의한 전달’보다는 ‘상수 객체 참조자에 의한 전달’ 방식을 택하는 편이 대개 낫다
- C++는 함수로부터 객체를 전달받거나 함수에서 객체 전달시 ‘값에 의한 전달’(c에서 물려받은 특성)
- 사본 -> 복사 생성자에 의해 생성 -> 값에 의한 전달이 고비용이 된다.
- 필요치 않은 생성자 / 소멸자 호출이 이루어진다.
- 상수 객체에 대한 참조자(reference to const)로 전달
- const를 붙임으로써 변화로부터 보호를 받는다
- 참조자를 호출하므로 생성자 / 소멸자 호출이 없다
- 복사 손실 문제(slicing problem)이 없어진다
- 복사 손실 문제 : 파생 클래스 객체가 기본 클래스 객체로서 전달될 때 값으로 전달되면 기본 클래스의 복사 생성자가 호출, 파생 클래스의 특징은 잘려나간다.
- 참조자는 포인터를 써서 구현이 된다.
- 타입이 기본 제공 타입일 경우 값으로 넘기는 편이 효율적일 때가 많다.
- 반복자와 함수 객체를 구현할 때 조심해야 할 점
- 복사 효율을 높일 것
- 복사 손실 문제에 노출되지 않도록 할 것
- 타입 크기가 작고 복사 생성자도 비싸지 않아도 수행 성능 문제가 있을 수 있다.
- ex) 컴파일러 중 기본 제공 타입 / 사용자 정의 타입을 아예 다르게 취급하는 게 존재.
- 진짜 double은 레지스터에 적재하나, double 하나 뿐인 객체는 레지스터 적재를 하지 않는다.
- 복사 생성자가 비쌀 수 있다.
- 데이터 멤버가 포인터 하나라도 포인터 멤버가 가리키는 대상까지 복사하는 작업도 따라온다.
- 사용자 정의 타입은 변화에 노출되어 있다
- 따라서 ‘값에 의한 전달’이 저비용이라 가정할 수 있는 타입들은
- 기본 제공 타입
- STL 반복자
- 함수 객체 타입