티스토리 뷰

 

Effective C++ 이펙티브 C++
스콧 마이어스 저/곽용재
 

4. 설계 및 선언

 

항목 18 : 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

  • 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터리로 쓰기에 어렵습니다. 인터페이스를 만들때는 이 특성을 지닐 수 있도록 고민하고 또 고민합시다.
  • 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본제공 타입과의 동작 호환성 유지하기가 있습니다.
  • 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기가 있습니다.
  • tr1::shared_ptr은 사용자 정의 삭제자를 지원합니다. 이 특징 때문에 tr1::shared_ptr은 교차 DLL 문제를 막아 주며, 뮤텍스 등 자동으로 잠금 해제하는 데(항목 14 참조) 쓸 수 있습니다.

 

항목 19 :  클래스 설계는 타입 설계와 똑같이 취급하자

  • 클래스 설계는 타입 설계입니다. 새로운 타입을 정의하기 전에, 이번 항목에 나온 모든 고려사항을 빠짐없이 점검해 보십시오.

새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?

객체 초기화는 객체 대입과 어떻게 달라야 하는가? (항목 4 참조)

새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?

새로운 타입이 가질 수 있는 적접한 값에 대한 제약은 무엇으로 잡을 것인가?

기존의 클래스 상속 계통망(inheritance graph)에 맞출 것인가? (항목 7, 34, 36 참조)

어떤 종류의 타입 변환을 허용할 것인가? (항목 15 참조)

어떤 연산자와 함수를 두어야 의미가 있을까? (항목 23, 24, 46 참조)

표준 함수들 중 어떤 것을 허용하지 말 것인가? (항목 6 참조)

새로운 타입의 멤버에 대한 접근 권한을 어느 쪽에 줄 것인가?

'선언되지 않은 인터페이스'로 무엇을 둘 것인가? (항목 29 참조)

새로 만드는 타입이 얼마나 일반적인가?

정말로 꼭 필요한 타입인가?

 

항목 20 : '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다

  • '값에 의한 전달'보다는 '상수 객체 참조자에 의한 전달'을 선호합시다. 대체적으로 효율적일 뿐만 아니라 복사손실 문제까지 막아 줍니다.
  • 이번 항목에서 다룬 법칙은 기본 제공 타입 및 STL 반복자, 그리고 함수 객체 타입에는 맞지 않습니다. 이들에 대해서는 '값에 의한 전달'이 더 적절합니다.

 

항목 21 : 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자

  • 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일, 또는 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대로 하지 마세요.

 

잘못된 예 1 

// 스택에 할당된 객체에 대한 참조자 반환

const Rational& operator*(const Rational& lhs, const Rational& rhs)

{

Rational result(lhs.n * rhs.n, lhs.d * rhs.d);

return result;

}

 

잘못된 예 2

// 힙에 할당된 객체에 대한 참조자 반환

const Rational& operator*(const Rational& lhs, const Rational& rhs)

{

Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);

return *result;

}

 

잘못된 예 3

// 지역 정적 객체에 대한 참조자 반환

const Rational& operator*(const Rational& lhs, const Rational& rhs)

{

static Rational result;

result = ......;

return result;

}

 

올바른 예

inline const Rational operator*(const Rational& lhs, const Rational& rhs)

{

return Rational(lhs.n * rhs.n, lhs.d * rhs.d);

}

 

항목 22 : 데이터 멤버가 선언될 곳은 private 영역임을 명심하자

  • 데이터 멤버는 private 멤버로 선언합시다. 이를 통해 클래스 제작자는 문법적으로 일관성 있는 데이터 접근 통로를 제공할 수 있고, 필요에 따라서는 세밀한 접근 제어도 가능하며, 클래스의 불변속성을 강화할 수 있을 뿐 아니라, 내부 구현의 융통성도 발휘할 수 있습니다.
  • protected는 public보다 더 많이 '보호'받고 있는 것이 절대로 아닙니다.

 

항목 23 : 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자

  • 멤버 함수보다는 비멤버 비프렌드 함수를 자주 쓰도록 합시다. 캡슐화 정도가 높아지고, 패키징 유연성도 커지며, 기능적인 확장성도 늘어납니다.

 

항목 24 : 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자

  • 어떤 함수에 들어가는 모든 매개변수(this 포인터가 가리키는 객체도 포함해서)에 대해 타입 변환을 해 줄 필요가 있다면, 그 함수는 비멤버이어야 합니다.

 

항목 25 : 예외를 던지지 않는 swap에 대한 지원도 생각해 보자

  • std::swap이 여러분의 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공합시다. 이 멤버 swap은 예외를 던지지 않도록 만듭시다.
  • 멤버 swap을 제공했으면, 이 멤버를 호출하는 비멤버 swap도 제공합니다. 클래스(템플릿이 아닌)에 대해서는, std::swap도 특수화해 둡시다.
  • 사용자 입장에서 swap을 호출할 때는, std::swap에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap을 호출합시다.
  • 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능합니다. 그러나 std에 어떤 것이라도 새로 '추가'하려고 들지는 마십시오.

 

 

 

- James Song

댓글