{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[C++] 스터디 CPPALOM 13주차: Effective C++ Chap 3~4

파워 포인트 파일(.pptx)을 Markdown으로 변환하여 업로드하였음)

# <br>CPPALOM

# <br>13주차 – Effective C++ Chap 3~4
한수빈

# <br>항목13: 자원 관리에는 객체가 그만!

* 동적 할당된 객체를 프로그래머가 직접 소멸시키는 것은 위험하다.
* RAII: Resource Acquisition Is Initialization.
  * 힙에 무언가를 할당해야 할 때에는, 자원 관리 객체가 소유하게 한다.
  * 첫째, 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
  * 둘째, 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
* 즉 자원 관리 객체는 스택에 선언되며, 스택에 선언된 자원 관리 객체는 자신이 소멸할 때 자동으로 자신이 갖고 있는 힙 자원들을 소멸시키게 된다.

* unique_ptr
* shared_ptr

![](img\week13_subin0.png)

![](img\week13_subin1.png)

# <br>항목14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자

* RAII 방식의 객체를 복사는 어떻게 정의되어야 하는가?
  * 가령, vector 객체를 복사한다.
* 복사를 금지한다.
  * Uncopyable.
  * 복사 생성자를 삭제한다.
* 관리하고 있는 자원에 대해 참조 카운팅을 수행한다.
  * shared_ptr
  * 카운트가 0이 되면 소멸시킨다.
* 관리하고 있는 자원을 진짜로 복사한다.
  * deep copy
* 관리하고 있는 자원의 소유권을 옮긴다.
  * unique_ptr
  * 단 하나의 객체만 해당 자원을 관리하도록 한다.

# <br>항목15: 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자

* 자원 관리 클래스는 자원을 감싸고 있는 객체이다.
* 많은 API들은 자원 관리 객체가 아닌 자원 그 자체에 관심이 있다.
  * 가령, shared_ptr 객체가 아닌 자원의 포인터를 인자로 받는 경우가 많을 것이다.
* 따라서 이러한 자원 관리 클래스들은 반드시 해당 자원에 직접 접근할 수 있는 통로를 제공해주어야 한다.
  * 스마트 포인터의 경우, get().
* 암시적 변환도 고려할 수 있다.
  * 자원 관리 클래스인 Font는 FontHandle로의 암시적 변환을 지원하여 changeFontSize가 자원인 FontHandle을 요구함에도 불구하고 정상적으로 처리되게 할 수 있다.

```C++
class Font {
    public:
    ...
    operator FontHandle() const
    { return f; }
    ...
};
```

```C++
Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize);
```

# <br>항목16: new 및 delete를 사용할 때는 형태를 반드시 맞추자

* new와 new[]는 다르다. 마찬가지로, delete와 delete[]는 다르다.
  * 단일 객체의 경우 메모리 구조는 다음과 같다:
  * 배열의 경우 메모리 구조는 다음과 같다:
* 따라서 new로 할당하였으면 delete를, new[]로 할당하였다면 delete[]를 적절히 호출해주어야 한다!
  * 만약 제대로 호출되지 않는다면 메모리 누수가 일어나거나, 의도치 않은 동작을 일으키게 된다.

| Object |
| :-: |


| n | Object | Object | Object | … |
| :-: | :-: | :-: | :-: | :-: |


# <br>항목17: new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자

* 다음 문장은 메모리 누수가 발생할 수 있다:
  * C++의 컴파일러는 식의 평가 순서가 자유롭다.
  * new Widget을 수행한 뒤, priority()를 수행할 수도 있다.
  * 이 경우, priority()에서 예외가 발생한다면 new Widget은 소멸되지 않고 메모리에 상주하게 된다.
* 따라서 예외의 발생을 최대한 피하도록 구문을 구성하는 것이 좋다!

```C++
processWidget(std::shared_ptr<Widget>(new Widget), priority());
```

```C++
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
```

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

* 인터페이스의 올바른 사용을 이끄는 방법은 다음과 같다:
  * 인터페이스 사이의 일관성을 잡아야 한다.
  * 기본제공 타입과의 동작 호환성을 유지해야 한다.
* 사용자의 실수를 방지하는 방법은 다음과 같다:
  * 새로운 타입을 만든다.
  * 타입에 대한 연산을 제한한다.
  * 객체의 값에 대해 제약을 건다.
  * 자원 관리 작업을 사용자 책임으로 놓지 않는다.
* 대표적인 예로 shared_ptr의 사용이 있다.

```C++
MyContainer a;
a.size();
MyCollection b;
b.length();
MyArray c;
c.length;
```

```C++
Date d(3, 30, 1995);
Date d(Month::Mar(), Day(30), Year(1995));
```

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

* 다음을 생각해보자:
  * 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?
    * 생성자와 소멸자
  * 객체 초기화는 객체 대입과 어떻게 달라야 하는가?
    * 생성자와 대입 연산자
  * 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?
    * 복사 생성자
  * 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?
    * 해당 타입이 반드시 만족해야 하는 조건(Invariant)
    * 가령, “Hour는 0 이상 24 미만의 정수이다.”
  * 기존의 클래스 상속 계통망에 맞출 것인가?
    * 상속
  * 어떤 종류의 타입 변환을 허용할 것인가?
    * 암시적 및 명시적 캐스팅

* 다음을 생각해보자:
  * 어떤 연산자와 함수를 두어야 의미가 있을까?
    * 클래스의 속성과 행위
  * 표준 함수들 중 어떤 것을 허용하지 말 것인가?
    * delete, private
  * 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가?
    * 접근 지시자, friend
  * ‘선언되지 않은 인터페이스'로 무엇을 둘 것인가?
    * 수행 성능, 예외 안전성, 자원 사용
  * 새로 만드는 타입이 얼마나 일반적인가?
    * 템플릿을 적용해야 하는지?
  * 정말로 꼭 필요한 타입인가?
    * 비멤버 함수, 템플릿을 몇 개 더 정의하는 편이 차라리 나은지?

# <br>항목20: ‘값에 의한 전달’ 보다는 ‘상수객체 참조자에 의한 전달 방식’을 택하는 편이 대개 낫다.

* 다음 코드는 많은 비용을 소모한다:
  * Student의 복사 생성자, Student 부모 클래스의 복사 생성자, Student 내 속성들의 복사 생성자 등.
* 대신, 참조로 넘기면 이러한 비용을 막을 수 있다.
  * 참조로 넘길 때의 문제는 클라이언트 측에서 해당 객체의 변화를 신경써야 한다는 것이다.
  * 따라서, const 키워드를 이용해서 그러한 일을 배제한다.
* 값에 대한 전달이 괜찮은 경우는 기본제공 타입, STL 반복자, 함수 객체 타입이다.
  * 그 외의 경우, 비용을 반드시 따져보고 결정하도록 하자!

```C++
bool validateStudent(Student s);
Student plato;
bool platoIsOK = validateStudent(plato);
```

```C++
bool validateStudent(const Student& s);
```

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

* 우측의 코드에서 operator*가 참조를 반환할 수 있는 적절한 방법은 존재하지 않는다.
  * 내부 함수에서 반드시 새로운 Rational 객체가 생성되어야 하고, 이들은 지역 객체이므로 복사가 필연적이다.
* 만약 억지로 참조를 반환하려고 할 경우, 메모리 누수나 성능 문제에서 취약해진다.
* 다음은 해당 객체가 두 개 이상 필요해질 경우 절대로 하지 말아야 한다:
  * 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일
  * 힙에 할당된 객체에 대한 참조자를 반환하는 일
  * 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일

```C++
class Rational {
public:
    Rational(
        int numerator = 0, 
        int denominator = 1
    );
private:
    int n, d;
    friend const Rational operator*(
        const Rational& lhs,
        const Rational& rhs
    );
};
```

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

* 캡슐화를 위해 반드시 세부 구현을 숨겨야 한다.
  * 우측 코드의 averageSoFar()는 구현이 어떻든 간에 클라이언트가 이를 신경쓸 필요가 없다.
  * 현재의 평균값을 유지하는 변수를 둔다거나, 함수가 호출될 때마다 평균값을 계산하는 방법 등의 다양한 구현에서 이것들이 변경되었을 때 영향을 받지 않는다.
* 또한 클래스 제작자는 문법적으로 일관적인 데이터 접근 통로를 제공할 수 있다.
* 필요에 따라서는 세밀한 접근 제어도 가능하며, 클래스의 불변속성을 강화할 수 있다.
* protected 역시 이러한 변화에 취약하며, public과 비슷한 정도의 고민을 해야 한다.

```C++
class SpeedDataCollection {
    ...
public:
    void addValue(int speed);
    double averageSoFar() const;
}
```

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

* WebBrowser의 멤버 함수 clearEverything()과 비멤버 비프렌드 함수인 clearBrowser()는 같은 기능을 수행한다.
  * 둘 중 무엇이 나을까?
* private 멤버 부분을 접근하는 함수는 최소화 하는 것이 캡슐화에 도움이 된다.
  * 해당 데이터를 조작할 방법이 적다는 것이고, 이는 더욱 안전하다는 이야기이다.
* 따라서, clearBrowser()의 경우가 더 낫다.
* 수빈의 첨언:
  * C++에서는 private 멤버를 직접 참조할 필요가 없는 helper function들의 경우 굳이 멤버 함수로 선언하지 않는 것이 바람직한 경우가 많다.

```C++
class WebBrowser {
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
    ...
    void clearEverything();
};
void clearBrowser(WebBrowser& wb)
{
    wb.clearCache();
    wb.clearhistory();
    wb.removeCookies();
}
```

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

* 우측과 같이 선언된 Rational의 경우, 다음 코드는 컴파일 될 수 없다.
  * * 연산자 우측 항의 경우 operator*의 선언에 의해 매개변수로 들어갈 수 있고, 암시적으로 변환될 수 있다.
  * 하지만 * 연산자 좌측 항의 경우 operator*의 선언에서는 this와 마찬가지이므로 암시적으로 변환될 수 없다.
* 따라서 비멤버 함수로 이를 선언한다면, 어떤 항에 대해서든 암시적으로 변환이 가능하므로 이러한 문제를 해결할 수 있다.
  * 주목할 점은 friend로 선언되지 않았다는 점이다!
  * private 데이터에 접근할 필요가 없기 때문이다.

```C++
class Rational {
public:
    Rational(
        int numerator = 0, 
        int denominator = 1
    );
private:
    int n, d;
    int numerator() const;
    int denominator() const;
    const Rational operator*(
        const Rational& rhs
    );
};
```

```C++
Rational oneHalf(1, 2);
Rational result = 2 * oneHalf;
```

```C++
const Rational operator*(
    const Rational& lhs, 
    const Rational& rhs);
```

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

* 수빈의 첨언:
  * 이 항목에서 이야기하는 swap에 대해서는 현대 C++과 들어맞지 않는 부분들이 존재한다.
  * 예를 들어, 현대 C++에서는 swap()의 구현을 C++11 이후에 추가된 rvalue reference의 활용으로 구현하고 있다.
  * 따라서 템플릿 특수화에 대한 이야기만 짚고 넘어가는 것이 좋을 것이다.
* std::swap을 오버로딩하고 싶을 때, 특수화를 이용할 수 있다.
  * 클라이언트는 해당 객체의 swap()의 특수화 여부를 모를 수 있다.
  * 클라이언트는 어떻게 적절히 swap()을 호출할 수 있을까?
  * using std::swap;을 작성하고, swap()을 호출하면 된다.
  * 이 경우, 함수 호출은 네임스페이스 내 함수 호출이 우선되므로,
  * 특수화가 되어 있는 경우에는 특수화 함수를 호출하며,
  * 그렇지 않은 경우에는 std::swap()을 호출하게 된다.

```C++
template<typename T>
void swap(
    Widget<T>& a,
    Widget<T>& b)
{
    a.swap(b);
}
template<typename T>
void doSomething(T& obj1, T& obj2)
{
    using std::swap;
    ...
    swap(obj1, obj2);
}
```

댓글

이 블로그의 인기 게시물

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링

[코딩의탑] 5층: 포탈(Portal), 더 나아가기