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

[C++] 스터디 CPPALOM 12주차: Effective C++ Chap 1~2

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

# <br>CPPALOM

# <br>12주차 – Effective C++ Chap 1~2
한수빈

# <br>항목1: C++를 언어들의 연합체로 바라보는 안목은 필수

* C++은 다중패러다임 프로그래밍 언어라고 불린다.
* 다음 네 가지 하위 언어를 제공한다:
  * C
  * 객체 지향 개념의 C++: 클래스를 쓰는 C
  * 템플릿 C++: 일반화 프로그래밍
  * STL: 템플릿 라이브러리

# <br>항목2: #define을 쓰려거든 const, enum, inline을 떠올리자

* 상수를 사용하기 위해서 #define을 사용하지 마라. 대신:
* 주의점: 다음은 컴파일 오류를 일으킨다.
* A::C는 선언만 되어있으며, 정의가 되어있지 않다.
  * 그러나 min()은 참조를 요구하기 때문에 오류가 난다.

```C++
const char* const authorName = “Scott Meyers”;
const std::string authorName(“Scott Meyers”);
```

```C++
#include <algorithm>
class A
{
public:
    static const int C = 10; // DECLARATION
};
// const int A::C;              DEFINITION
int main()
{
    std::min(0, A::C);
}
```

# <br>enum hack

* const 객체에 대해 주소를 취하는 것을 방지할 수 있다.
* 많은 코드에서 이 기법이 쓰이고 있으므로 이러한 방식에 대해 익혀 놓아야 한다.
* 수빈의 첨언:
  * 현대 C++에서 단순히 상수를 이용하고 싶을 때에는 constexpr를 권장한다!

```C++
class GamePlayer
{
    private:
    enum { NumTurns = 5};
    int scores[NumTurns];
};
```

# <br>#define의 위험성

* f가 호출되기 전에 a가 증가하는 횟수가 달라진다.
  * 위의 경우 2번 증가, 아래의 경우 1번 증가한다.
  * ++a가 더 크다면, ++a가 f의 인자로 들어가기 때문이다.

```C++
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
int main(){
    int a = 5, b = 0;
    CALL_WITH_MAX(++a, b);
    CALL_WITH_MAX(++a, b+10);
}
```

# <br>항목3: 낌새만 보이면 const를 들이대 보자!

* 소스 코드 수준에서 불변을 결정할 수 있다.
  * 다음과 같은 참사를 막을 수 있다.
* 만약 어떤 변수가 변하지 않는다면, 그냥 const를 붙여라.
  * const correctness!

```C++
class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);
…Rational a, b, c;
…(a * b) = c;
…if (a * b = c)
```

# <br>상수 멤버 함수

* 멤버 함수에도 역시 const를 붙일 수 있다.
  * const 객체여도 해당 함수들은 호출할 수 있다!
* 비트수준 상수성
  * 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 ‘const’임을 인정하는 개념이다.
  * 하지만 멤버 변수가 포인터일 경우, 해당 포인터의 값만 변경하지 않는다면 const가 여전히 인정되므로 이에 주의해야 한다!
* 논리수준 상수성
  * 클래스 바깥에서 볼 때 해당 객체의 행위가 여전히 동일하다면, 그 멤버 함수가 ‘cosnt’임을 인정하는 개념이다.

# <br>mutable

* 논리 수준 상수성을 구현 가능하게 만든다.
  * length()는 분명 멤버 변수를 변경시키지만 이는 내부 구현을 위한 변경일 뿐, 논리적인 객체의 상태를 변경시키는 것은 아니다.
  * mutable 키워드를 지닌 변수들은 const 멤버 함수에서도 변경될 수 있다.

```C++
class CTextBlock
{
public:
    ...
    std::size_t length() const;
private:
    char *pText;
    mutable std::size_t textLength;
    mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const{
    if(!lengthIsValid) {
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }
    return textLength;
}
```

# <br>const 멤버 함수 코드 중복의 해결책

* const_cast를 통해서 멋지게 해결할 수 있다.
* 반드시 비상수 멤버 함수가 상수 멤버 함수를 호출하는 형태로 구현되어야 한다!
  * 비상수 멤버 함수는 상수 멤버 함수를 호출할 수 있지만,
  * 상수 멤버 함수는 비상수 멤버 함수를 호출하면 안 되기 때문이다.

```C++
char& operator[](std::size_t position)
{
    return const_cast<char&>(
        static_cast<const TextBlock&>(*this)[position]
    );
}
```

# <br>항목4: 객체를 사용하기 전에 반드시 그 객체를 초기화하자

* 초기화가 될 지 안 될 지 모르니 명시적으로 반드시 초기화를 수행하라!
* 대입과 초기화를 구분해서 하자:
* 기본 클래스는 파생 클래스보다 먼저 초기화된다.
* 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화된다.
  * 멤버 초기화 리스트에 순서가 바뀌어 기재되었더라도, 반드시 선언된 순서대로 초기화된다.

```C++
ABEntry::ABEntry(const std::string& name, const std::string& address,
    const std::list<PhoneNumber>& phones)
{
    theName = name;
    theAddress = address;
    thePhones = phones;
    numTimesConsulted = 0;
}
ABEntry::ABEntry(const std::string& name, const std::string& address,
    const std::list<PhoneNumber>& phones)
        : theName(name),
        theAddress(address),
        thePhones(phones),
        numTimesConsulted(0)
{ }
```

# <br>정적 객체와 번역 단위

* 정적 객체
  * 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체이다.
  * 전역 객체, 네임스페이스 유효범위에서 정의된 객체, 클래스 안에서 static으로 선언된 객체, 함수 안에서 static으로 선언된 객체, 파일 유효범위에서 static으로 정의된 객체
  * 이들 중 함수 안에 있는 정적 객체는 지역 정적 객체, 나머지는 비지역 정적 객체라고 한다.
* 번역 단위
  * 컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드이다. 기본적으로 소스 파일 하나이다.
* 문제:
  * 별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 ‘정해져 있지 않다‘.

# <br>초기화 순서를 결정하는 법

* Singleton!
* FileSystem이 필요할 때마다 tfs()를 호출하여 활용한다.
  * 단 하나의 객체만을 보장하게 되며, 초기화 역시 사용 이전에 반드시 수행되게 된다!

```C++
FileSystem& tfs()
{
    static FileSystem fs;
    return fs;
}
```

# <br>항목5: C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자

* 자동으로 선언해주는 함수들이 있다:
  * (생성자)
  * 복사 생성자
  * 복사 대입 연산자
  * 소멸자
* 이들에 대해서 항상 주의하자!
  * 혹 기본 함수들을 활용할 수 있더라도, default 키워드를 이용해서 직접 선언하자.

```C++
CTextBlock() = default;
CTextBlock(const CTextBlock& rhs) = default;
~CTextBlock() = default;
CTextBlock& operator=(const CTextBlock& rhs) = default;
```

# <br>항목6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자

private으로 해당 함수들을 비공개하자

복사 생성자와 대입 연산자를 비공개한 Uncopyable 클래스를 기본 클래스로 활용하는 것도 한 방법이다:

```C++
class HomeForSale
{
public:
private:
    HomeForSale(const HomeForSale&);
    HomeForSale& operator=(const HomeForSale&);
};
```

```C++
class Uncopyable
{
protected:
    Uncopyable() = default;
    ~Uncopyable() = default;
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
};
```

```C++
class HomeForSale : private Uncopyable
```

# <br>항목7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자

* getTimeKeeper()는 TimeKeeper의 파생 클래스를 반환한다.
  * 이 경우, ptk의 소멸은 TimeKeeper의 부분만을 소멸시킨다.
  * 이를 방지하기 위해서 가상 소멸자를 선언할 수 있다.
* 가상 소멸자는 남용하면 안 된다!
  * virtual table을 유지하기 위해서 추가적인 메모리가 소모된다.
  * 가상 함수가 하나라도 들어 있다면 가상 소멸자를 선언하자.

```C++
TimeKeeper *ptk = getTimeKeeper();
delete ptk;
```

```C++
class TimeKeeper 
{
public:
    TimeKeeper();
    virtual ~TimeKeeper();
}
```

# <br>순수 가상 소멸자를 이용한 추상 클래스

* 어떤 클래스는 추상 클래스였으면 좋겠는데 마땅히 추가할 순수 가상 함수가 없을 때도 있다.
  * 순수 가상 소멸자를 선언하자!
* 단, 반드시 정의 부분을 추가해주어야 한다.

```C++
class AWOV 
{
public:
    virtual ~AWOV() = 0;
};
AWOV::~AWOV() {}
```

# <br>항목8: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자

* 소멸자에서 예외가 발생하는 것은 좋지 않다.
  * vector, list 등의 컨테이너에서 이들을 감당하는 것은 결코 쉽지 않다.
* 가령, 자원 관리 클래스들은 소멸자에서 자신이 관리하는 자원들을 소멸시킨다:
* 하지만 close() 호출이 예외를 발생시킨다면, 이는 문제가 발생할 수 있다.

```C++
class DBConn
{
public:
    ~DBConn()
    {
        db.close();
    }
private:
    DBConnection db;
}
```

# <br>예외를 처리하는 방법

프로그램을 바로 끝낸다.

예외를 삼킨다.

둘 다 좋은 전략은 아니다.

```C++
~DBConn()
{
    try { db.close() }
    catch (...) {
        // log
        std::abort();
    }
}
```

```C++
~DBConn()
{
    try { db.close() }
    catch (...) {
        // log
    }
}
```

* close 호출의 책임을 DBConn의 사용자 역시 질 수 있는 기회를 줘야 한다.
  * 물론, 사용자는 무시해도 된다.
* 중요한 것은, 예외에 대해서 그것을 능동적으로 사용자가 처리할 수 있는 창구를 만들어냈다는 것이다!

```C++
class DBConn
{
public:
    void close()
    {
        db.close();
        closed = true;
    }
    ~DBConn()
    {
        if (!closed)
            try { db.close() }
            catch (...) {
                // log
            }
    }
private:
    DBConnection db;
    bool closed;
}
```

# <br>항목9: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자

* Transaction의 생성자에서 가상 함수인 logTransaction()을 호출한다.
  * 이는 문제가 되는데, Transaction의 생성자 호출 시점에는 파생 클래스인 BuyTransaction의 초기화가 되어 있지 않기 때문이다.
  * 따라서 가상 함수임에도 불구하고 Transaction::logTransaction()이 호출된다.
* 이는 예상치 못한 동작들을 만든다!
  * 생성자에서 init() 등을 호출해 우회적으로 가상 멤버 함수를 호출하는 경우에 주의하라.

```C++
class Transaction
{
public:
    Transaction()
    {
        …
        logTransaction();
    }
    virtual void logTransaction() const = 0;
}
class BuyTransaction : public Transaction{
public:
    void logTransaction() const override;
};
```

# <br>항목10: 대입 연산자는 *this의 참조자를 반환하게 하자

* C++의 대입 연산은 여러 개가 사슬처럼 엮일 수 있다.
  * 우측 연관이므로, 다음 두 문장은 같다.
* 이는 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있기 때문이다.
  * 이는 일종의 관례이며, 우리도 지키는 것이 좋다!

```C++
x = y = z = 15; 
x = (y = (z = 15));
```

```C++
class Widget
{
public:
    Widget& operator=(const Widget& rhs)
    {
        …
        return *this;
    }
};
```

# <br>항목11: operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자

자기대입이란 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말한다.

다음 코드는 자기대입에서 문제가 발생한다.

```C++
class Widget
{
public:
    Widget &operator=(const Widget &rhs)
    {
        delete pb;
        pb = new Bitmap(*rhs.pb);
        return *this;
    }
private:
    Bitmap *pb;
};
```

# <br>자기대입의 해결책

일치성 검사

순서 변경하기

사본 만들기

멋지게 사본 만들기

```C++
Widget &operator=(const Widget &rhs)
{
    if (this == &rhs) return *this;
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}
```

```C++
Widget &operator=(const Widget &rhs)
{
    Widget temp(rhs);
    swap(temp);
    return *this;
}
```

```C++
Widget &operator=(const Widget &rhs)
{
    Bitmap *pOrig = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOrig;
    return *this;
}
```

```C++
Widget &operator=(const Widget rhs)
{
    swap(rhs);
    return *this;
}
```

# <br>항목12: 객체의 모든 부분을 빠짐없이 복사하자

클래스의 멤버 변수를 수정했다면, 반드시 그와 관련한 복사 생성자, 복사 대입 연산자 역시 수정해야 한다.

파생 클래스의 복사 생성자 및 복사 대입 연산자를 구현할 경우, 반드시 부모의 함수 역시 호출해주어야 한다.

복사 생성자와 복사 대입 연산자에서 공통된 부분이 있다면, 함수로 분리해보는 것도 좋다.

```C++
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    : Customer(rhs),
    priority(rhs.priority)
{ 
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{ 
    Customer::operator=(rhs);
    priority = rhs.priority;
    return *this;
}
```

댓글

이 블로그의 인기 게시물

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

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

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