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

[C++] 스터디 CPPALOM 14주차: Effective C++ Item 26~35

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


# <br>CPPALOM

# <br>14주차 – Effective C++ item 26~35
한수빈

# <br>항목26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자

* 변수는 사용되기 직전에 정의되어야 한다.
  * 예외 등으로 인해 해당 변수가 사용되지 않음에도 소멸자 등이 호출되는 비용이 발생되기 때문이다!
* 루프에서 변수를 다루는 방법
  * 루프 바깥쪽에 정의하기: 생성자 1번 + 소멸자 1번 + 대입 n번
  * 루프 안쪽에 정의하기: 생성자 n번 + 소멸자 n번

```C++
Widget w;
for (int i = 0; i < n; ++i)
{
    w = i;
    ...
}
```

```C++
for (int i = 0; i < n; ++i)
{
    Widget w(i);
}
```

# <br>항목27: 캐스팅은 절약, 또 절약! 잊지 말자

* 구형 스타일의 캐스트를 사용하지 말자!
* dynamic_cast는 성능 저하가 있으므로, 이를 최소화하자!
  * 최악: 폭포식 dynamic_cast
* 수빈의 첨언:
  * 상속 구조에서 특정한 타입인지를 반드시 알아야되는 구조라면, 리스코프 치환 원칙(LSP) 위배일 가능성이 높다. dynamic_cast는 필요한 경우가 있지만 이에 주의하자!

```C++
for(auto iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
    if(SpecialWindow1 *psw1 =
        dynamic_cast<SpecialWindow*>(iter->get()))
    {...}
    else if (SpecialWindow2 *psw2 =
        dynamic_cast<SpecialWindow2*>(iter->get()))
    {...}
    else if (SpecailWindow3 *psw3 =
        dynamic_cast<SpecialWindow3*>(iter->get()))
    {...}
    ...
}
```

# <br>항목28: 내부에서 사용하는 객체에 대한 ‘핸들'을 반환하는 코드는 되도록 피하자

* 추상화된 객체에서 세부 구현에 대한 포인터를 반환하지 말자.
* upperLeft(), lowerRight()는 const 함수임에도 불구하고 외부의 클라이언트가 해당 RectData를 조작할 수 있게 만든다.
  * 분명 Rectangle 클래스에 대한 비트적 상수성은 지켜지고 있으나, 논리적 상수성을 위배하게 된다!
  * 따라서 상수 포인터를 넘기는 것 이외에는 이러한 행위를 자제하자.

```C++
class Rectangle {
public:
    Point& upperLeft() const { return pData->ulhc; }
    Point& lowerRight() const { return pData->lrhc; }
private:
    std::shared_ptr<RectData> pData;    
}
```

```C++
class Rectangle {
public:
    const Point& upperLeft() const { return pData->ulhc; }
    const Point& lowerRight() const { return pData->lrhc; }
private:
    std::shared_ptr<RectData> pData;    
}
```

* 또한 dangling handle을 반환할 여지가 있다.
* boundingBox()를 통해 반환된 Rectangle 객체는 임시 객체이다.
  * 임시 객체의 핸들이 upperLeft()를 통해 반환되고,
  * 그것이 클라이언트의 포인터 변수인 pUpperLeft에 대입된다.
  * 임시 객체는 수명을 다하였으므로 소멸되며,
  * pUpperLeft는 dangling pointer가 된다.

```C++
const Rectangle boundingBox(const GUIObject& obj);
…
GUIObject *pgo;
const Point *pUpperLeft =
    &(boundingBox(*pgo).upperLeft());
```

# <br>항목29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!

* 다음 코드는 최악의 예외 안전성을 보여준다!
* 예외 안전성을 가진 함수라면 예외가 발생할 때 다음과 같이 동작해야 한다:
  * 자원이 새도록 만들지 않는다.
  * 자료구조가 더럽혀지는 것을 허용하지 않는다.

```C++
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    lock(&mnutex);
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgeSrc);
    unlock(&mutex);
}
```

* 자원 관리 객체를 이용해서 mutex의 unlock 문제를 해결할 수 있다.
* 예외 안전성을 갖춘 함수는 다음 세 가지 중 하나를 보장한다.
  * 기본적인 보장: 함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련한 모든 것들을 유효한 상태로 유지한다.
  * 강력한 보장: 함수 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않는다.
  * 예외불가 보장: 예외를 절대로 던지지 않는다.
* 강력한 보장을 지키는 것이 적절한 타협안이 될 것이다.

```C++
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock ml(&mutex);
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgeSrc);
}
```

기본적인 보장을 지키도록 바꾼 코드는 다음과 같다:

스마트 포인터가 객체의 생명주기를 안전하게 관리하므로, reset() 호출 이전과 reset() 호출 이후만이 원자적인 상태로서 남는다.

```C++
std::shared_ptr<Image> bgImage;
…
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock ml(&mutex);
    bgImage.reset(new Image(imgeSrc));
    ++imageChanges;
}
```

* 강력한 보장을 지키도록 바꾼 코드는 다음과 같다:
* copy-and-swap 기법은 언제나 사본을 만들고 그것을 조작한 뒤에 마지막에 원본과 바꿔치기 하는 것이다.
  * 이는 강력한 보장을 지키기에 적절한 기법이다.

```C++
std::shared_ptr<PMImpl> pImpl;
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    using std::swap;
    Lock ml(&mutex);
    // copy
    std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
    // manipulate
    pNew->bgImage.reset(new Image(imgeSrc));
    ++pNew->imageChanges;
    // swap
    swap(pImpl, pNew);
}
```

* 하지만 모든 함수의 예외 안전성이 강력하다는 보장이 없기 때문에, 다음과 같은 코드에서는 문제가 발생한다:
  * f1()과 f2()가 강력한 예외 안전성을 보장하지 않으므로, side effect가 발생한다.
  * 이는 현실적인 문제이며, 실제로 이러한 문제 때문에 강력한 예외 안전성을 보장하기 어렵다.
* 하지만 우리는 최대한 강력한 예외 안전성을 보장하려고 노력해야 한다!

```C++
void someFunc()
{
    ...
    f1();
    f2();
    ...
}
```

# <br>항목30: 인라인 함수는 미주알고주알 따져서 이해해 두자

* 먼저, 인라인의 여부는 컴파일러에게 달렸음을 명심하자.
* 인라인의 trade-off:
  * 인라인은 함수 코드를 그대로 각각의 함수 호출 코드에 삽입해버리는 것이므로,
  * 함수 호출의 오버헤드를 줄일 수 있다.
  * 단, 목적 코드가 비정상적으로 불어날 수 있다.
  * 곧, 캐싱에도 효율이 떨어질 수 있으며 성능 저하로 이어질 수 있다.
* 클래스 선언 시 직접 함수를 정의하면 암시적인 인라인이 된다!
  * 혹은 inline 키워드를 사용하면 된다.
* 함수 인라인은 작고, 자주 호출되는 함수에 대해서만 하는 것으로 생각하자.
  * 인라인을 남용하지 말 것!

# <br>항목31: 파일 사이의 컴파일 의존성을 최대한 줄이자

* 객체를 생성하기 위해서는 할당을 위해 메모리 구조를 알아야 한다.
  * 어떤 객체들을 멤버로 가지고 있는지: 다음 코드는 컴파일 될 수 없다.
  * stirng, Date, Address가 어떻게 정의되어 있는지 모르기 때문이다!
* 이러한 컴파일 의존성은 빌드 타임을 증가시킨다.
  * #include 지시자의 헤더들이 하나라도 바뀌게 되면, 해당 파일은 다시 컴파일되어야 한다!

```C++
class Person {
public:
    Person(const std::string& name, const Date& birthday, const Address& adr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::String theName;
    Date theBirthDate;
    Address theAddress;
}
```

* 좋은 해결책: 포인터를 사용하자!
  * 포인터는 전부 같은 크기를 가지며, 따라서 메모리에 할당할 때 별도의 객체 구조를 알 필요가 없다.
  * pimpl 관용구: Java의 interface를 구현하는 하나의 해결책
* 구현이 바뀌었을 때 PersonImpl 클래스가 컴파일이 다시 되는 것은 당연하다!
  * 단, Person을 include하는 수많은 클래스들은 다시 컴파일될 필요가 없다.

```C++
#include <memory>
#include <string>
class PersonImpl;
class Date;
class Address;
class Person {
public:
    Person(const std::string& name, const Date& birthday, const Address& adr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::shared_ptr<PersonImpl> pImpl;
};
```

* 객체 참조자 및 포인터로 충분한 경우에는 객체를 직접 쓰지 않는다.
  * 포인터 정의 시 해당 타입의 선언부만 include 한다.
* 할 수 있으면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 만든다.
  * 특히, 함수를 선언할 때는 클래스의 정의를 가져오지 않아도 된다.
* 오해 금물: today()를 사용하려면 결국 Date 클래스의 정의를 include 해야 한다.
  * 이는 today(), clearAppointments()의 사용자 측에서 필요에 따라 하게 된다!
  * 어떤 라이브러리를 만들었을 때, 수많은 함수들을 제공하게 된다.
  * 이들을 전부 사용하지 않음에도 라이브러리에 관련된 모든 클래스를 컴파일해야 하는 필요를 없앤다.

```C++
class Date;                         // 클래스 전방 선언
Date today();                       // OK, Date 클래스의
void clearAppointments(Date d);     // 정의를 가져오지 않아도 된다.
```

* 선언부와 정의부에 대해 별도의 헤더 파일을 제공한다.
  * 사용자는 선언부만을 includ해서 사용한다.
* 예시:
  * Main.cpp
  * Person.h
  * PersonImpl.h
  * Person.cpp

```C++
#include <string>
#include "Date.h"
class PersonImpl {
public:
    PersonImpl(const std::string& name, const Date& birthday);
    std::string name() const;
    std::string birthDate() const;
private:
    std::string theName;
    Date theDate;
};
```

```C++
#include "Person.h"
#include "Date.h"
int main()
{
    Person p("Subin", Date(30));
    …
}
```

```C++
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string& name, const Date& birthday)
    : pImpl(new PersonImpl(name, birthday))
{
}
std::string Person::name() const
{
    return pImpl->name();
}
…
```

```C++
class PersonImpl;
class Date;
class Person {
public:
    Person(const std::string& name, const Date& birthday);
    std::string name() const;
    std::string birthDate() const;
private:
    std::shared_ptr<PersonImpl> pImpl;
};
```

* 인터페이스 클래스와 팩토리 함수를 활용할 수도 있다.
* 사용자는 추상 클래스인 Person에만 의존하며 세부 구현은 동적으로 할당되어 활용된다!
* 선언과 정의의 분리에 대한 비용:
  * 핸들 클래스의 경우: 포인터에 대한 메모리와 성능, 생성자 및 소멸자 호출 오버헤드
  * 인터페이스의 경우: 가상 테이블 오버헤드
  * 공통: 인라인 함수의 도움을 얻기 어려움!

```C++
class Person {
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
    static std::shared_ptr<Person>
    create(
        const std::string& name,
        const Date& birthday,
        const Address& addr);
};
```

# <br>항목32: public 상속 모형은 반드시 is-a를 따르도록 만들자

* 자식 클래스는 부모 클래스여야 한다.
  * 부모 클래스는 자식 클래스이지 않아도 된다.
  * 가령, 학생(자식)은 사람(부모)이지만, 사람(부모)는 학생(자식)이 아닐 수도 있다.
* 주의해야 할 사항: 프로그램 관점에서의 is-a는 현실과 다를 수도 있다!
  * 리스코프 치환 원칙(LSP)를 지켜야 한다.
  * 다음은 리스코프 치환 원칙이 지켜지지 않는다: 정사각형은 사각형이지만, 프로그램 관점에서 정사각형의 변 길이를 다루는 것이 부모의 구현과 충돌한다.

```C++
class Rectangle {
public:
    virtual void setHeight(int newHeight);
    virtual void setWidth(int newWidth);
    virtual int height() const;
    virtual int width() const;
}
void makeBigger(Rectangle& r)
{
    int oldHeight = r.height();
    r.setWidth(r.width() + 10);
    assert(r.height() == oldHeight);
}
```

```C++
class Square : public Rectangle{};
Square s;
assert(s.width() == s.height());
makeBigger(s);
assert(s.width() == s.height());
```

# <br>항목33: 상속된 이름을 숨기는 일은 피하자

* 오버로딩된 함수를 자식 클래스에서 오버라이딩할 때에는 주의하라!
  * 파생 클래스의 이름은 기본 클래스의 이름을 가린다. public 상속에서 이러한 이름 가림 현상은 바람직하지 않다.
  * 가려진 이름을 다시 볼 수 있게 하는 방법으로 using 선언 혹은 전달 함수를 쓸 수 있다.
* 이름 가림
  * 다음과 같은 상황에서, 클라이언트는 Derived 객체의 mf1(int)를 호출할 수 없다.

```C++
class Base {
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    ...
};
class Derived : public Base {
public:
    virtual void mf1();
}
```

```C++
Derived d;
int x;
d.mf1();    // OK. Call the Derived::mf1().
d.mf1(x);   // Error! Derived::mf1(int) is not defined.
```

* 오버로딩된 함수를 자식 클래스에서 오버라이딩할 때에는 주의하라!
  * 파생 클래스의 이름은 기본 클래스의 이름을 가린다. public 상속에서 이러한 이름 가림 현상은 바람직하지 않다.
  * 가려진 이름을 다시 볼 수 있게 하는 방법으로 using 선언 혹은 전달 함수를 쓸 수 있다.
* 전달 함수
  * 상황: private 상속을 하였으며 오버로딩된 mf1 중 단 하나만 사용하게 만들고 싶다.
  * 만약 using Base::mf1;을 활용할 경우 오버로딩된 mf1(int)도 드러나게 된다.
  * 대신 전달 함수를 활용해 하나만 활성화시킬 수 있다.

```C++
class Base {
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    ...
};
class Derived : private Base {
public:
    virtual void mf1()
    { Base::mf1(); }}
```

# <br>항목34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자

* 순수 가상 함수:
  * 파생 클래스에게 함수의 인터페이스만을 물려준다.
  * 즉, 동적으로 정의되어야 하는 동작!
* 단순 가상 함수:
  * 파생 클래스에게 인터페이스뿐만 아니라 그 함수의 기본 구현도 물려준다.
  * 즉, 필요에 의해 정의되어야 하는 동작!
* 비가상 함수:
  * 함수 인터페이스와 더불어 그 함수의 필수적인 구현을 물려준다.
  * 즉, 변하지 않는 동작!

* 편법:
  * 순수 가상 함수이면서도 기본 동작을 제공하는 법
  * 순수 가상 함수로 선언하고, 정의를 제공해버린다!

```C++
class Airplane {
public:
    virtual void fly(const Airport& destination) = 0;
};
void Airplane::fly(const Airport& destination)
{
    // defaultFly();
}
class ModelA : public Airplane {
public:
    virtual void fly(const Airport& destination)
    { Airplane::fly(destination); }
};
```

# <br>항목35: 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자

* 템플릿 메서드 패턴 활용하기
  * GameCharacter의 개발자는 healthValue를 반환하는 일종의 템플릿을 만들고, 핵심 로직은 파생 클래스들이 재정의할 수 있도록 한다.
  * 이를 NVI(non-virtual interface) 관용구라고도 한다!

```C++
class GameCharacter {
public:
    int healthValue() const
    {
        ...
        int retVal = doHealthValue();
        ...
        return retVal;
    }
private:
    virtual int doHealthValue() const
    {
        …    }
}
```

* 전략 패턴 활용하기
  * 함수 포인터를 이용해서 healthValue를 반환하는 함수를 동적으로 생성자를 통해 받는다.
  * 일반적인 객체지향 디자인 패턴과 달리, C++에서는 객체가 아닌 함수 포인터로 받아도 된다!
  * 더 나아가, <functional> 템플릿을 이용해 좀 더 유연한 함수를 전달받을 수 있다.
    * 가령, 일반적으로 클래스의 멤버로 정의된 함수를 GameCharacter의 생성자에 넘겨줄 수는 없다. 함수 호출 규약이 다르기 때문이다.
    * std::bind를 이용해 this 인자를 무시하고 단항 함수로 만들어 멤버 함수임에도 넘길 수 있게 된다!

```C++
class GameCharacter {
public:
   typedef int (*HealthCalcFunc)(const GameCharacter&);
   explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
   : healthFunc(hcf)
   {}
   int healthValue() const{ return healthFunc(*this); }
private:
    HealthCalcFunc healthFunc;
}
```

댓글

이 블로그의 인기 게시물

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

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

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