본문 바로가기
프로그래밍언어/C++

[Effective C++] 항목 18 ~ 21

by 목가 2018. 1. 23.
반응형

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

class Date {
public:
 Date(int month, int day, int year);
 ...
};
 - 매개변수 전달 순서 오류
 - 숫자 범위 오류
 1) Wrapper Type을 구현하여, 오류를 막자

struct Month{
 explict Month(int m) : val(m) {}
 int val;
};

class Date {
public:
 Date(const Month& m, const Day& d, const Year& y);
 ...
};
Date d(Month(1), Day(3), Year(2018));


 2) 유효한 값의 범위를 제한하자
  - enum 활용 => 타입 안전성?

  - 유효한 값의 집합을 미리 정의해서 사용하자

class Month{
public:
 static Month Jan() {return Month(1);}
 static Month Feb() {return Month(2);}
 ...
 static Month Dec() {return Month(12);}
private:
 explict Month(int m);
};


Date d(Month::Mar(), Day(30), Year(2018));

 3) 타입에 제약을 부여하자
  - "const" 활용 : operator*의 반환 타입을 const로 정의
 if(a*b = c)    // Compile Error

 4) 일관성 있는 인터페이스를 제공하자
  - 인터페이스의 이름 통일
  - 뭔가 외워야 제대로 쓸 수 있는 인터페이스는 잘못 쓰기 쉽다 => 강제하자

 5) 자원 해제 문제에 관한 사용자 실수를 사전 봉쇄하자
 Invertment* createInvestment();    // 사용후 포인터 삭제필요
  ...
 Invertment* pInv = createInvestment();
 delete pInv;
  - shared_ptr을 사용하자
 std:tr1::shared_ptr<Investment> createInvestment();
  ...
 std:tr1::shared_ptr<Investment> pInv = createInvestment();
  - 만약, 별도의 함수에서 shared_ptr을 삭제하고자 하는 경우, 별도의 함수와 shared_ptr을 묶자
 std::tr1::shared_ptr<Investment> pInv(static_cast<Investment*>(0), getRidOfInvestment);

  * 교차 DLL 문제 (cross-DLL problem)
  - 객체 생성 시 A라는 DLL의 new연산자를 이용했지만 해제시에는 B라는 DLL의 delete연산자를 이용하는 경우에 발생
  - new/delete 짝이 실행되는 DLL이 달라서 꼬이게 되면 대다수의 플랫폼에서 에러가 발생
  - shared_ptr를 이용하면 해결되는데, shared_ptr가 생성된 DLL과 동일한 DLL에서 delete를 호출하도록 내부적으로 작성되어있기 때문

* 좋은 인터페이스는 "제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 고민하자
인터페이스의 올바른 사용을 위해 일관성 잡아주기, 기본제공 타입과의 동작 호환성 유지하기가 있다
사용자의 실수를 방지하기 위해, 새로운 타입 만들기, 타입에 대한 연산 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 자동으로 하기
tr1::shared_ptr의 사용자 정의 삭제자를 활용하여, 교차 DLL 문제해결 및 뮤텍스 자동 잠금 해제 등에 사용하자

 참고) shared_ptr의 생서자 함수는 크게 세가지
 
  1. template<class _Ux>
  2. explicit shared_ptr(_Ux *_Px)
  3. {       // construct shared_ptr object that owns _Px
  4.         _Resetp(_Px);
  5. }
  6.  
  7. template<class _Ux, class _Dx>
  8. shared_ptr(_Ux *_Px, _Dx _Dt)
  9. {       // construct with _Px, deleter
  10.         _Resetp(_Px, _Dt);
  11. }
  12.  
  13. template<class _Ux, class _Dx, class _Alloc>
  14. shared_ptr(_Ux *_Px, _Dx _Dt, _Alloc _Ax)
  15. {       // construct with _Px, deleter, allocator
  16.         _Resetp(_Px, _Dt, _Ax);
  17. }

 참고 : http://egloos.zum.com/sweeper/v/2826435


항목 19 : 클래스 설계는 타입 설계와 똑같이 취급하자
 1) 좋은 클래스 설계
  - 자연스러운 문법(syntax)
  - 직관적인 의미구조(semantics)
  - 효율적인 구현이 한가지 이상 가능(?)

 2) 설계시 고민해야 될 것
  - 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?
  - 객체 초기화는 객체 대입과 어떻게 달라져야 하는가?
  - 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?
  - 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?
  - 기존의 클래스 상속 계통망(inheritance graph)에 맞출 것인가? (virtual)
  - 어떤 종류의 타입 변환을 허용할 것인가?
  - 어떤 연산자와 함수를 두어야 의미가 있을까?
  - 표준 함수들 중 어떤 것을 허용하지 말 것인가? (private)
  - 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가? (freind?)
  -'선언되지 않은 인터페이스'로 무엇을 둘 것인가? (?) (제공할 보장 - 수행성능 및 예외 안잔성, 자원 사용)
  - 새로 만드는 타입이 얼마나 일반적인가? (class template)
  - 정말로 꼭 필요한 타입인가?

 클래스 설계는 타입 설계이다. 새로운 타입을 정의하기 전에 모든 고려사항을 점검하자


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

 1) Pass-by-value
  - 인자의 사본을 통한 처리 => 복사 생성자
  - 고비용 처리
class Persion {
public:
 Person();
 ...
private:
 std::string name;
 std::string address;
};
class Studunt : public Persion {
public:
 Studunt();
 ...
private:
 std::string schoolName;
 std::string schoolAddress;
};

bool validateStudunt(Studunt s);    // Prototype

Studunt plato;
bool platoIsOk = validateStudunt(plato);    // 
 - 매개변수 전달 비용 계산:
   => Studunt 생성자/소멸자 + String(schoolName)생성자/소멸자 + String(schoolAddress) 생성자/소멸자
   + Person 생성자/소멸자 + String(name) 생성자/소멸자 + String(address) 생성자/소멸자

 2) Pass-by-reference-to-const
 bool validateStudunt(const Studunt& s);    // Prototype
 - 생성자와 소멸자가 호출되지 않는다
 - 호출부에서 변화된 내용을 보호 받기 위해 "const"를 붙힌다
 - 복사손실 문제(slicing problem) 방지
class Window {
public:
 std::string name() const;
 virtual void display() const;
};
class WindowWithScrollBars : public Windows {
public:
 virtual void display() const;
};

void printNameDisplay(Window w)
{
  std::cout << w.name();
  w.display();
}

WindowsWithScrollBars wwsb;
printNameDisplay(wwsb);      //Window 생성자가 호출되어 Window::display() 호출

void printNameDisplay(const Windows& w)
{
  std::cout << w.name();
  w.display();
}

WindowsWithScrollBars wwsb;
printNameDisplay(wwsb);      //Window 생성자가 호출되어 WindowsWithScrollBars::display() 호출

 3) 인자 타입에 따른 사용 선택
  - 객체의 타입이 기본제공 타입(int 등)STL 반복자, 함수 객체 타입 경우, Pass-by-value가 더 효율적일 수 있다.
  => 타입의 크기가 작다고 효율적인 것은 아님 (컴파일러중에는 기본제공 타입과 사용자 정의 타입을 다르게 취급 하는게 있다)

 * '값에 의한 전달'보다는 '상수 객체 참조자에 의한 전달'을 선호합시다
 * 기본제공 타입, STL 반복자, 함수 객체 타입은 "값에 의한 전달을 사용합시다.


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

- operator*등의 함수에서 참조자를 반환하는건 시간 낭비이다
- 새로운 객체를 반환하자
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rotional(lhs.n*rhs.n, lhs.d*rhs.d);
}

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


반응형

'프로그래밍언어 > C++' 카테고리의 다른 글

[Effective C++] 항목 26~28  (0) 2018.04.10
[Effective C++] 항목 22 ~ 25  (0) 2018.01.23
[Effective C++] 항목 13 ~ 17  (0) 2018.01.23
[Effective C++] 항목 5 ~ 12  (0) 2018.01.23
[Effective C++] 항목1 ~ 4  (0) 2018.01.23

댓글