이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 142p ~ 149p
요약
항목 18: 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
- 예상되는 사용자 실수를 막는 다른 방법으로는 어떤 타입이 제약을 부여하여 그 타입을 통해 할 수 있는 일들을 묶어 버리는 방법이 있다.
- 제약 부여 방법으로 아주 흔히 쓰이는 예가
const
붙이기 이다.
// operator*의 반환 타입을 const로 한정하면 다음과 같은 실수를 방지할 수 있다.
if (a * b = c) ...
- 특별한 이유가 없다면 사용자 정의 타입은 기본제공 타입처럼 동작하게 만드는 것이 좋다.
- 이렇게 하는 실질적인 이유는 일관성 있는 인터페이스를 제공하기 위함이다.
Investment* createInvestment();
- 사용자 입장에서 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘못 쓰기 쉽다.
createInvestment
함수는 어떤 객체를 동적 할당하고 그 객체의 포인터를 반환하는 함수이다.- 이 함수를 이용하는 사용자는 객체의 사용을 완료했으면 반드시 포인터를 삭제해야 한다.
- 따라서 팩토리 함수에서 포인터를 리턴하는 것이 아니라 RAII 객체를 리턴하면 사용자는 함수를 호출해서 객체를 이용하기만 하면 된다.
std::tr1::shared_ptr<Investment> createInvestment();
shared_ptr
에 삭제자를 적용하고 싶으면 다음과 같이 작성해주면 된다.pInv
로 관리할 실제 객체의 포인터를 결정하는 시점이pInv
을 생성하는 시점보다 앞서는 경우에는, 아래 코드처럼 null로 초기화하고 나중에 대입하는 방법보단pInv
의 생성자에 바로 넘기는게 좋다.
// getRidOfInvestment: 삭제자 지정 함수
tr1::shared_ptr<Investment> createInvestment()
{
// 컴파일 에러, shared_ptr의 첫 번째 인자는 실제 관리할 자원의 포인터가 되어야 함
// std::tr1::shared_ptr<Investment> pInv(0, getRidOfInvestment);
// null 포인터를 가진 shared_ptr 생성은 static_cast 사용
std::tr1::shared_ptr<Investment> pInv(static_cast<Investment*> 0, getRidOfInvestment);
pInv = ... // 실제 객체 가리키는 작업
return pInv
}
포인터별(per-pointer) 삭제자 지정
tr1::shared_ptr
에는 포인터별로 삭제자를 자동으로 사용하여 사용자의 실수를 미연에 방지할 수 있게 해준다.- 교차 DLL 문제
- 객체 생성 시에 어떤 DLL의 new를 썼는데, 그 객체를 삭제할 때는 이전의 DLL과 다른 DLL에 있는 delete를 썼을 경우에 발생한다.
- new와 delete 짝이 실행되는 DLL 달라서 꼬이게 되면 대다수의 플랫폼에서 런타임 에러가 발생한다.
tr1::shared_ptr
은 기본 삭제자가tr1::shared_ptr
이 생성된 DLL과 동일한 DLL에서 사용하도록 만들어져 있다.
std::tr1::shared_ptr<Investment> createInvestment()
{
return std::tr1::shared_ptr<Investment>(new Stock);
}
Stock
이라는 클래스가Investment
의 파생 클래스일 때, 이 함수에서 반환하는tr1::shared_ptr
은 다른 DLL들 사이에 이리저리 넘겨지더라도 교차 DLL 문제를 걱정하지 않아도 된다.- Stock 객체를 가리키는
tr1::shared_ptr
은 그 Stock 객체의 참조 카운트가 0이 될 때 어떤 DLL의 delete를 사용해야 하는지를 꼭 붙들고 잊지 않는다.
- Stock 객체를 가리키는
항목 19: 클래스 설계는 타입 설계와 똑같이 취급하자
- 새로운 클래스를 정의한다는 것은 새로운 타입을 하나 정의하는 것과 같다.
- 함수와 연산자 오버로드
- 메모리 할당 및 해제
- 객체 초기화 및 종료처리
- 좋은 타입은 일단 문법이 자연스럽고, 의미구조가 직관적이며, 효율적인 구현이 한 가지 이상 가능해야 한다.
- 클래스를 설계할 때 자주 마주하는 질문은 다음과 같다.
새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?
- 이 부분이 어떻게 되느냐에 따라 클래스 생성자 및 소멸자의 설계가 바뀐다.
- 메모리 할당 함수를 직접 설계하는 경우에는 이들 함수의 설계에도 영향을 미친다.
- operator new, operator new[], operator delete, operator delete[]
객체 초기화는 개체 대입과 어떻게 달라야 하는가?
- 생성자와 대입 연산자의 동작 및 둘 사이의 차이점을 결정짓는 요소이다.
- 초기화와 대입을 헷갈리지 않는 것이 가장 중요하며, 각각에 해당되는 함수 호출이 아예 다르다는 점을 인지해야 한다.
새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?
- 값에 의한 전달을 구현하는 부분은 복사 생성자이다.
새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?
- 전부는 아니지만, 클래스의 데이터 멤버의 몇 가지 조합 값만은 반드시 유효해야 한다.
- 이런 조합을 클래스의 불변속성이라고 하며, 클래스 차원에서 지켜주어야 하는 부분이다.
- 이 불변속성에 따라 클래스 멤버 함수 안에서 해 주어야 할 에러 점검 루틴이 좌우된다.
- 가장 영향을 많이 받는 것은
생성자
,대입 연산자
, 그리고 각종 쓰기 함수(setter)
이다. - 이 밖에 예외에도 영향을 미치며, 예외 지정을 쓴다면 그 부분에도 영향을 준다.
- 가장 영향을 많이 받는 것은
기존의 클래스 상속 계통망에 맞출 것인가?
- 이미 갖고 있는 클래스로부터 상속을 시킨다고 하면, 당연히 파생 클래스는 기본 클래스에 의해 제약을 받게 된다.
- 특히 멤버 함수가 가상인가, 비가상인가의 여부가 가장 크다.
어떤 종류의 타입 변환을 허용할 것인가?
- 암시적으로 변환되도록 할 것인가, 아니면 명시적 타입 변환만 허용할 것인가?
- 암시적 허용을 할 것이면 T1 클래스를 T2 클래스로 변환할 수 있게 해준다.
- 명시적 허용만 가능하면 별도의 함수를 만들되, 타입 변환 연산자 혹은 비명시 호출 생성자는 만들지 말아야 한다.
어떤 연산자와 함수를 두어야 의미가 있을까?
- 클래스 안에 선언할 함수가 바로 여기서 결정된다.
- 어떤 것들은 멤버 함수로 적당할 것이고, 또 몇몇은 그렇지 않을 것이다.
표준 함수들 중 어떤 것을 허용하지 말 것인가?
- private으로 선언해야 하는 함수가 여기에 해당된다.
새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가?
- 어떤 클래스 멤버를
public
,protected
,private
영역에 둘 것인가를 결정하는 데 도움을 주게 될 질문이다. - 프렌드로 만들어야 할 클래스 및 함수를 정하는 것은 물론이고 한 클래스를 다른 클래스에 중첩시켜도 되는가에 대한 결정을 내리는 데도 도움이 될 것이다.
선언되지 않은 인터페이스로 무엇을 둘 것인가?
- 사용자 정의 타입이 제공할 보장이 어떤 종류일까에 대한 질문으로, 보장할 수 있는 부분은 수행 성능 및 예외 안전성 그리고 자원 사용이다.
- 이들에 대해 보장하겠다고 결정한 결과는 클래스 구현에 있어서 제약으로 작용한다.
새로 만드는 타입이 얼마나 일반적인가?
- 실제로는 타입 하나를 정의하는 것이 아니라 동일 계열의 타입군 전체일지도 모른다.
- 이런 경우에는 클래스 템플릿을 정의해야 한다.
정말로 꼭 필요한 타입인가?
- 기존의 클래스에 대해 기능 몇 개가 아쉬워서 파생 클래스를 만드는 것이라면, 차라리 간단하게 비멤버 함수나 템플릿을 몇 개 더 정의하는 편이 낫다.
메모
항목 18: 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
- 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터리로 쓰기에 어렵다. 인터페이스를 만들 때는 이 특성을 지닐 수 있도록 고민하고 또 고민해야 한다.
- 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본제공 타입과의 동작 호환성 유지하기가 있다.
- 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기가 있다.
tr1::shared_ptr
은 사용자 정의 삭제자를 지원한다. 이 특징 때문에tr1::shared_ptr
은 교차 DLL 문제를 막아 주며, 뮤텍스 등을 자동으로 잠금 해제하는 데 쓸 수 있다.
항목 19: 클래스 설계는 타입 설계와 똑같이 취급하자
- 클래스 설계는 타입 설계이다. 새로운 타입을 정의하기 전에, 이번 항목에 나온 모든 고려사항을 빠짐없이 점검해보자.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(160p ~169p) (1) | 2024.01.21 |
---|---|
이펙티브 C++(149p ~ 160p) (0) | 2024.01.19 |
이펙티브 C++(132p ~ 142p) (0) | 2024.01.17 |
이펙티브 C++(121p ~ 132p) (0) | 2024.01.16 |
이펙티브 C++(111p ~ 121p) (0) | 2024.01.15 |