[Effective C++ 3판] Chapter 4. 설계 및 선언 (항목 18~25)

Chapter 4. 설계 및 선언 (항목 18~25)

하하하 하루에 2개씩 포스팅한다고 해놓고 한 개도 안했었네요~ ㅎㅎㅎ 하하하ㅏㅎㅎㅎ하하

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

class BadDate {
public:
    BadDate(int month, int day, int year);
};

class Month {
public:
    static Month Jan;    // Incorrect!
    static const Month& Feb() {    // Case 1
        static Month m(2);
        return m;
    }
    static Month Mar() { return Month(3); }    // Case 2

private:
    explicit Month(int m)
        : month(m) {}
    int month;
};
Month Month::Jan(1);

struct Day {
    explicit Day(int d)
        : day(d) {}
    int day;
};

struct Year {
    explicit Year(int y)
        : year(y) {}
    int year;
};

class Date {
public:
    Date(Month month, Day day, Year year);
};

int main() {
    BadDate badDate(1996, 2, 2);    // mistake of user!
    Date date(Month::Feb(), Day(2), Year(1996));
}

  위 코드를 봐보자. BadDate의 경우 날짜를 그냥 int로만 받으면 사용자의 실수로 인해 parameter의 순서가 바뀌는 일이 생길 수 있다. 따라서 사용자가 명시적으로 타입을 지정하도록 강제하면 사용자에게 실수를 컴파일타임에 알려줄 수 있다. 따라서 각각 Month, Day, Year에 해당하는 클래스(혹은 구조체)를 만들고, 생성자를 explicit으로 하면 사용자가 Year(1996)과 같이 타입과 값을 명시하도록 함으로서 실수를 방지할 수 있다. 여기서 더 나가가서 사용자에게 유효값만을 강제하고 싶을 경우, 위 코드의 Month 클래스처럼 유효한 Month의 집합을 미리 정의해놓으면 좋다. 이 때 static으로 객체들의 집합을 미리 정의해 놓으면 안된다. (비지역 정적 객체의 초기화 순서를 예측할 수 없기 때문에) 그리고 집합 객체를 읽기만 했을 경우 새로운 객체 생성를 막기 위해서 Case 1과 같이 할 경우 비지역 정적 객체의 초기화 여부를 check하기 위한 코드가 함수 처음에 들어가므로 이에 따른 overload가 있다. 또한 멀티쓰레드 환경에서 동기화문제가 발생하지 않을 경우에만 사용가능하다. 따라서 Case 1과 Case 2중 뭐를 써야 할지는 그때 그때 다를 것 같다. (동기화 문제가 발생하지 않고 생성자에서 하는 일이 많고, 읽기만 하는 경우가 많을 때 Case 1이 더 좋을 것이다.) 가장 범용적이고 안정적으로 쓰려면 Case 2의 방법을 쓰는게 낫다.

[교차 DLL 문제]
  특정 객체를 new한 DLL과 delete하는 DLL이 다를 경우 문제가 발생할 수 있다. 왜냐하면 DLL별로 사용하는 힙이 다를 수 있기 때문이다. 해결책은 클래스에 operator new, delete를 overload하던가, std::shared_ptr 생성자의 2번째 인자(custom deleter)를 사용하는 해결책이 있다. 근데 VS2012부터는 CRT가 기본적으로 process heap을 사용한다고 한다. (이전에는 heap을 생성) 음... 왠만한 경우에 문제가 발생하진 않을 것 같긴한데... 잘 모르겠다... 시간상 자료를 많이 찾아보지 못했다. 개인적인 생각에는 그래도 교차 DLL문제가 발생한다고 생각하고 설계를 일단 하는게 맞는거 같다.
http://stackoverflow.com/questions/443147/c-mix-new-delete-between-libs
http://stackoverflow.com/questions/21625330/does-msvcrt-uses-a-different-heap-for-allocations-since-vs2012-2010-2013
http://stackoverflow.com/questions/30346144/about-across-dll-boundaries-delete-pointer-in-vc2013

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

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

[고려사항]
- 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?
- 객체 초기화는 객체 대입과 어떻게 달라야 하는가?
- 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가? (복사 생성자)
- 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가? (불변속성. 생성자/대입 연산자/setter함수등에 영향 많이 줌.)
- 기존의 클래스 상속 계통망에 맞출 것인가?
- 어떤 종류의 타입 변환을 허용할 것인가? (명시적/암시적. 명시적 타입 변환만 허용하고 싶을 경우에는 타입 변환 연산자 혹은 비명시호출 생성자는 만드지 말고 변환을 맡는 별도 함수만 만듬.)
- 어떤 연산자와 함수를 두어야 의미가 있을가?
- 표준 함수들 중 어떤 것을 허용하지 말 것인가? (private)
- 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가?
- '선언되지 않은 인터페이스'로 무엇을 둘 것인가?
- 새로 만드는 타입이 얼마나 일반적인가? (동일 계열의 타입군일 경우는 클래스 탬플릿을 정의해야한다.)
- 정말로 꼭 필요한 타입인가? (기존에 있는 클래스에 함수 몇 개를 추가하여 할 수는 없나? 굳히 새로 클래스를 만들어야하는 것인가? 를 고민.)

[이것만은 잊지 말자]
- 클래스 설계는 타입 설계입니다. 새로운 타입을 정의하기 전에, 이번 항목에 나온 모든 고려사항을 빠짐없이 점검해 보십시오.

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

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

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

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

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

데이터 멤버를 함수 인터페이스 뒤에 감추게 되면 구현상의 융통성을 가질 수 있다. 그리고 책에서 인상 깊었던 말이 있는데 한번 인용해보겠다.
public이란 '캡슐화되지 않았다'는 뜻이며, 실질적인 측면에서 이는 곧' 바꿀 수 없다'라는 의미를 담고 있다.
즉, 데이터 멤버를 public으로 만들 게 되면 나중에 데이터 멤버를 삭제하거나 변수이름을 변경하거나 타입을 변경하는 등의 작업이 힘들어지게 된다. 만약 라이브러리나 프레임워크 개발이라면 불가능하다고 봐야한다. 따라서 데이터 멤버는 private로 최대한 캡슐화하고 인터페이스 함수를 제공하는게 옳다고 할 수 있다. 

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

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

일반적으로 직관적으로 생각했을 때, 클래스와 연관된 함수들은 클래스의 멤버 함수로 두어야할 것 같다. 하지만 실질적으로 만약 비멤버 비프렌드 함수로 둘 수 있는 경우에는 비멤버 비프렌드 함수로 두는 것이 캡슐화 정도가 높아지고 패키징 유연성이 커진다. 왜냐하면 비멤버 비프렌드 함수는 멤버함수보다 접근권한이 약하고(public만 접근할 수 있음) 해당 클래스와 해당 함수를 파일분할(컴파일 의존성을 줄여준다. 컴파일 시간 감소!) 할 수 있기 때문이다.
class Example {
public:
    void doA() {}
    void doB() {}
    void doC() {}
    void doAll() {  // Bad!
        doA();
        doB();
        doC();
    }
};

void doAll(Example &ex) {  // Good!
    ex.doA();
    ex.doB();
    ex.doC();
}

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

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

암시적인 형변환을 지원하는 클래스가 있다고 하자. 이 클래스에 대해서 operator*을 멤버함수로서 overload했다고 하자. 이경우에는 A * B라고 했을 때 A위치에 대해서는 암시적 형변환이 지원되지 않는다. 따라서 이런 경우에는 operator*을 비멤버함수로서 overload해야한다.

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

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

이 항목은 template에 대한 내용이 많이 등장함으로 이에 대해서 익숙하지 않으신 분들은 책을 읽으면서 헤매셨을수 도 있습니다. 관련 template내용에 대한 보충자료로서 http://egloos.zum.com/sweeper/v/2998778 를 추천합니다. 설명이 깔끔하고 명확하게 잘 되어있습니다. (강추!)

[std namespace에 대한 템플릿 제한사항]
- std namespace에 대해서 프로그래머가 직접 만든 타입에 대해 표준 템플릿을 완전 특수화하는 것은 허용된다.
- std namespace에 대해 프로그래머가 새로운 템플릿을 정의를 하는 것은 금지된다. (따라서 template overload도 못함.) (컴파일/실행 됨. 근데 실행결과가 미.정.의.사.항...Damn?!)
- 더 있을 수도 없을 수도... (궁금한 사람들은 검색해보세요)
namespace ExampleStuff {
    template <typename T>
    class ExampleImpl {
    };

    template <typename T>
    class Example {
    public:
        void swap(Example &ex) {
            using std::swap;
            swap(pImpl, ex.pImpl);
        }
    private:
        ExampleImpl<T> *pImpl;
    };

    template <typename T>
    void swap(Example<T> a, Example<T> b) {
        a.swap(b);
    }
}

template <typename T>
void func(T& a, T& b) {
    using std::swap;
    swap(a, b);  // find function by argument-dependent lookup.
}

클래스에 대한 swap을 마련할 생각이라면, 단순히 std::swap에 대한 완전 특수화 함수를 정의하면 된다. 하지만 클래스 템플릿에 대한 swap 특수화 버전을 마련할 때는 다른 방법을 써야한다. (함수 템플릿에 대한 부분 특수화는 불가능하기 때문에) 해결책은 위 코드와 같다. func함수에서 일어나는 일은 다음과 같다. 인자 기반 탐색에 의해 인자 타입과 동일한 namespace에서 swap의 특수화 버전을 먼저 탐색하게 되고, 없을 경우 using에 의해 std namespace에서 탐색하게 된다. 따라서 우리가 원하는 목적을 이룰 수 있다. 문제는 class를 사용하는 사용자가 swap을 사용할 때 func함수와 같이 사용할 것을 기대해야만 한다는 것이다. 사용자가 만약 std::swap()을 사용한다면 다 무용지물이 된다. 그래서 완벽한 방법은 아닌 것 같다... 더 좋은 해결책은 없을까??? (비야네 날 보고있다면 정답을 알려줘!)

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



후... 드디어 포스팅 끝... 포스팅하는데 엄청 오래걸리네,,, 책 읽는거보다 2배정도 걸리는 거 같습니다...ㅠㅜㅜ 그래도 포스팅하면서 다시 복습도 하고 좀 더 완벽하게 이해하고,, 정리도 해둘 수 있어서 좋긴 한데... 시간을 너무 잡아 먹는군요 ㅠㅜ 하루 2개 포스팅은 무슨;; 하루에 1개도 힘들군요 ㅋㅋㅋㅋㅋ 그래도 내일 포스팅 또 해보도록 하겠습니다~
그나저나 네이버 smart editor 3.0 코드 하이라이팅이 너무 맘에 안들군요... 아직 html편집도 지원을 안해서 외부 코드 하이라이팅을 쓸 수도 없고,,, 이거 원.... ㅠㅜ

댓글