ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective C++] 설계 및 선언
    프로그래밍 기초/C++ 2022. 7. 13. 07:51

      설계 및 선언 

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

    새로운 타입을 만들어 실수 막기

    인터페이스에서 요구하는 특정한 타입이 존재하는 경우 사용자가 실수하더라도 쉽게 그 실수를 인지할 수 있게 된다.


    class Date {

    public:

        Date(int month, int day, int year); // 단순히 int를 사용하는 경우

    };

     

    Date(13, 2, 2022); // 입력 순서를 틀리거나

    Date(2, 40, 2022); // 존재하지 않는 값을 넘기게 될 수 있다.


     

    위와 같은 경우를 다음과 같이 년, 월, 일을 나타내는 타입을 도입하면 막을 수 있게 된다.


    struct Day;

    struct Month;
    struct Year;

     

    class Date {

    public:

        Date(const& Month month, const& Day day, const& Year year); // 단순히 int를 사용하는 경우

    };

     

    Date(30, 3, 1995); // 타입이 맞지 않아 에러 발생

    Date(Day(30), Month(3), Year(1995)); // 타입(순서)이 맞지 않아 에러 발생

    Date(Month(3), Day(30), Year(1995)); // 정상 동작


     

    추가로 이렇게 타입을 고정시킬 수 있고 입력이 제한되는 경우 미리 값을 정의해 사용할 수 있도록 하여 실수를 막을 수 있다.


    class Month {

    public:

        static Month Jan() { return Month(1); }

        static Month Fab() { return Month(2); }

        static Month Mar() { return Month(3); }

        ...

    private:

        explicit Month(int m); // 외부에서 생성하지 못하도록 막는다.

    };


     

    명확한 이유가 없으면 기본 타입의 동작을 따라가자

    기본형의 동작은 대부분의 프로그래머가 정확하게 이해하고 있으며 다른 타입도 그렇게 동작하기를 기대한다.

    따라서 명확하게 그렇게 동작해야만 하는 게 아니면 기본형 동작을 따라가야 한다.


    if (a * b = c) // 이런 동작이 발생하지 않도록 하자


     

    사용자가 무언가 따로 해줘야만 하지 않도록 할 것

    기본적으로 사용자는 어떤 동작을 처리하고 다음에 무엇을 해 줘야 하는지 기억을 못 하는 경우가 많다.

    따라서 기억하지 않아도 되도록 자동 처리가 되도록 해야한다.


    Investment* createInvestment(); // 사용 후 사용자가 자원을 삭제해줘야 하는 구현 대신

    std::tr1::shared_ptr<Investment> createInvestment(); // 알아서 자원이 삭제되도록 구현한다.


     

    ※ 스마트 포인터 중 shared_ptr을 사용한다면 소멸할 때 특정 함수를 호출하도록 삭제자를 지정할 수 있다.

     

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

    클래스 설계 시의 질문거리

    C++에서 새로운 클래스를 정의하는 것은 새로운 타입을 정의하는 것과 같다.

    따라서 좋은 클래스를 만들기 위해서는 좋은 타입을 만드는 것 만큼의 고민과 노력이 필요하다.

    • 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가? 클래스 생성자 및 소멸자의 설계
    • 객체 초기화는 객체 대입과 어떻게 달라야 하는가? 두 함수의 동작 및 차이점 결정
    • 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가? 복사 생성자 구현
    • 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가? 멤버 변수 중 어떤 값이 유효해야 하는지 결정
    • 기존의 클래스 상속 계통망에 맞출 것인가? 가상 함수로 만들 것인지 여부
    • 어떤 종류의 타입 변환을 허용할 것인가? 암시적 변환이 필요한지 명시적 변환이 필요한지에 따라 결정
    • 어떤 연산자와 함수를 두어야 의미가 있을까? 클래스에 선언할 함수 결정
    • 표준 함수들 중 어떤 것을 허용하지 말 것인가? private으로 선언할 함수 결정
    • 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가? public/protected/private/friend의 권한 결정
    • '선언되지 않은 인터페이스'로 무엇을 둘 것인가? 클래스가 안전하다고 보장할 수 있는 영역* 설정
    • 새로 만드는 타입이 얼마나 일반적인가? 클래스 템플릿이 필요한 것인지 여부
    • 정말로 꼭 필요한 타입인가? 굳이 클래스로 만들 필요가 있는지 여부

    * 보장할 수 있는 부분은 수행 성능예외 안전성 그리고 자원 사용이 있다.


     

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

    값에 의한 전달이 문제가 되는 이유

    기본적으로 값에 의한 전달은 복사 생성자가 호출되게 된다.

    그런데 상속받은 클래스이고 내부에 멤버 변수가 많다면 그만큼 복사 생성 호출이 늘어나게 된다.

     

    다음의 경우에 함수 호출 한 번에 생성자, 소멸자가 각각 여섯 번씩 호출된다.


    class Person {

    private:

        std::string name;

        std::string address;

    };

     

    class Student : public Person {

    private:

        std::string schoolName;

        std::string schoolAddress;

    };

     

    bool validateStudent(Student s);

     

    Student plato;

    bool platoIsOK = validateStudent(plato);


     

    참조에 의한 호출 시 이점

    소멸자, 생성자를 비효율적으로 호출하지 않는 것 외에도 복사 손실 문제*를 방지할 수 있다는 장점이 있다.


    * 기본 클래스로 변환해서 값으로 넘기는 경우 클래스의 다형성이 손상되는 문제를 말한다.

     

    가령 Person과 Student에 동일하게 Print 함수가 있다면 Student를 Person으로 값에 의한 호출을 한 경우 Print함수는   Person의 Print로 동작하게 된다.


     

    값에 의한 전달이 저비용인 경우

    다음의 세 가지만 값에 의한 전달이 저비용이라고 가정해도 되며 나머지는 참조를 사용하는 것이 더 낫다.

    • 기본 제공 타입
    • STL 반복자
    • 함수 객체 타입

     

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

    참조자를 반환할 때의 문제점

    참조자를 반환하는 경우 지역 변수를 반환하거나 힙에 객체를 할당한 뒤 반환하려고 할 수 있으나, 이 두 방법 모두 문제가 있다.

    • 지역 변수 참조자 반환 지역 변수는 범위를 벗어나면 소멸된다. 따라서 함수를 벗어나 반환된 참조자는 의미 없는 정보가 된다.
    • 힙 할당 객체 참조자 반환 힙에 할당된 객체는 반드시 delete를 호출해 줘야 하나 사용자가 적절하게 사용할지 보장할 수 없다.

     

    따라서 새로운 객체를 반환하는 게 옳다.


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

    {

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

    }


     

    ※ 몇몇 조건하에서는 최적화 메커니즘을 통해 반환 값의 생성자, 소멸자 호출이 큰 오버헤드를 가지지 않게 구현되므로 최적화는 컴파일러에게 맡기자.

     

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

    멤버 변수를 private으로 사용할 때의 장점

    세 가지의 장점이 존재한다.

    • 문법적 일관성 어떤 건 멤버 변수를 직접 호출하고 어떤 건 함수를 사용하는 대신 일괄적으로 함수를 사용하도록 할 수 있다.
    • 접근성의 정교한 제어 가능 읽기 전용, 읽기 쓰기 모두 가능, 쓰기 전용 등으로 접근 권한을 제어할 수 있다.
    • 캡슐화를 통한 코드 제어 다른 코드에 영향을 주지 않고 내부 구조를 수정할 수 있다.

     

    멤버 변수는 protected도 안 된다

    public과 달리 protected는 조금 더 안전하다고 생각하지만 해당 변수를 삭제하는 상황을 가정하면 그렇지도 않다는 것을 알 수 있다.

    • public 변수를 삭제하면 해당 변수를 사용하는 모든 코드에 영향을 주게 될 것이다.
    • protected 변수를 삭제하면 해당 클래스를 상속받는 모든 클래스에 영향을 주게 될 것이다.

    즉, 어떤 경우라도 피해를 파악하고 대응하기 쉽지 않다.

     

    ※ 캡슐화 관점에서 의미 있는 접근 수준은 private과 private이 아닌 나머지로 구분될 것이다.

     

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

    비멤버 비프렌드 함수를 사용해야 하는 이유

    어떤 클래스의 private 멤버 부분을 접근할 권한이 없는 함수이기 때문에 코드의 복잡도를 늘리지 않는다.

     

    비멤버 비프렌드 함수를 사용하는 방법

    여러 방법이 존재하지만 대표적으로 다음의 방법이 존재한다.

    • 같은 헤더의 전역 함수로 사용 가장 단순하지만 필요한 함수를 찾기 어려울 수 있다.
    • 정적 클래스의 정적 함수로 사용 관련된 모든 내용을 하나의 클래스에서 작성해야 하는 문제가 있다.
    • 네임 스페이스 내에서 전역 함수로 사용 필요한 내용의 구분이 가능하고 쉽게 확장할 수 있다.

     

    ※ 멤버 함수의 반대는 프렌드 함수가 아니라 비멤버 함수이다.

     

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

    표준 라이브러리 swap의 동작

    표준 라이브러리에 있는 swap은 다음과 같이 구현되는데, 한 번 호출에 복사가 세 번 일어난다.


    namespace std {

        template<typename T>

        void swap(T& a, T& b)

        {

            T temp(a);

             a = b;

             b = temp;

        }

    }


     

    일반적으로는 표준 라이브러리의 swap으로 충분하지만 대부분의 멤버가 포인터로 구성되어 있는 경우라면 복사 생성자를 호출하는 대신 포인터가 가리키는 대상만 바꿔주는 편이 더 효율적이므로 이런 경우엔 전용 swap을 구현하는 게 좋다.

     

    전용 swap 구현 방법

    전용 swap을 구현할 때는 다음과 같은 구현이 필요하다.

    • public 멤버 함수 swap 두 객체의 값을 맞바꾸는 예외를 던지지 않는 함수
    • 같은 네임스페이스의 비멤버 swap 멤버 함수 swap을 호출하는 함수

     

    사용자 관점의 swap 함수 사용

    사용자가 swap을 호출할 때는 전용 swap이 있으면 그것을 사용하고 그렇지 않으면 표준 라이브러리의 swap을 사용하도록 하려고 할 때 다음과 같이 사용하면 된다.


    template<typename T>

    void doSomething(T& obj1, T& obj2)

    {
       using std::swap;

        ...

        swap(obj1, obj2);

         ...

    }


    ※ 컴파일러는 인자 기반 탐색을 통해 특수화된 함수를 검색하고 없는 경우 using으로 선언된 표준 라이브러리 함수를 사용한다.

     

    댓글

Designed by Tistory.