관리 메뉴

KoreanFoodie's Study

Effective C++ 정리 5-1 : 구현 본문

Tutorials/C++ : Advanced

Effective C++ 정리 5-1 : 구현

머니덕 2022. 9. 15. 18:02
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

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

핵심 :

변수 정의는 늦출 수 있을 때까지 늦추자. 프로그램이 더 깔끔해지며 효율도 좋아진다!


다음 예시를 보자.

std::string encryptPassword(const std::string& password)
{
  using namespace std;
  string encrypted;
  
  // 1. encrypted 와 관련 없는 일
  ...
  
  // 2. encrypted 와 관련 있는 일
  ...
  
  return encrypted;
}

1번에서 예외가 발생해서 의미없게 encrypted 를 사용할 수도 있다. 이는 비효율적이다! 또한 생성 후 값을 대입하지 말고 생성과 동시에 초기화를 하면 쓸데없는 기본 생성자 호출 비용을 줄일 수 있을 것이다.

그렇다면 루프의 경우는 어떨까?

Widget w;
for (int i = 0; i < n; ++i)
{
  w = i에 따라 달라지는 값;
  ...
}

루프 바깥쪽에 w 를 선언하면 생성자, 소멸자 1번 + 대입 n 번의 비용이 든다. 루프 안쪽에 선언하면 생성자와 소멸자를 n 번 호출해야 한다. 이 경우에는 루프 바깥쪽에 변수를 선언하는 방법이 더 좋을 수도 있다.
하지만 '대입이 생성자-소멸자 쌍보다 비용이 덜 들고', '전체 코드에서 수행 성능에 민감한 부분을 건드리는 중' 이 아니라면, 루프 안쪽에 변수를 선언하는 것이 좋다!

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

핵심 :

1. 다른 방법이 가능하다면 캐스팅은 피하자. 특히 수행 성능에 민감한 코드에서 dynamic_cast 는 몇 번이고 다시 생각하자. 설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해 보자.
2. 캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있도록 해 보자. 이렇게 하면 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고 이 함수를 호출할 수 있게 된다.
3. 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호하자. 발견하기도 쉽고, 설계자가 어떤 역할을 의도했는지가 더 자세히 드러난다.


구형 스타일의 캐스트는 "(T) expr" , "T (expr)" 처럼 괄호를 쓴다. C++ 에서는 4 가지의 캐스트 방식이 있다 :

  • const_cast : 객체의 상수성(constness)를 없애는 용도로 사용됨
  • dynamic_cast : '안전한 다운캐스팅(safe downcasting)' 을 할 때 사용함 런타임 비용이 높음
  • reinterpret_cast : 포인터를 int 로 바꾸는 등의 하부 수준 캐스팅을 위해 만들어진 연산자로, 적용 결과가 구현환경에 의존적임. 이런 캐스트는 하부 수준 코드 외에는 거의 없어야 함
  • static_cast : 암시적 변환(비상수 객체를 상수 객체로, 혹은 int를 double 로 등)을 강제로 진행할 때 사용. 타입 변환을 거꾸로 수행하는 용도로도 쓰임(void* 를 일반 타입의 포인터로).

구형 스타일의 캐스트를 쓰는 경우는 다음과 같은 경우 말고는 찾아보기 힘들다.

class Widget
{
  public:
    explicit Widget(int size);
};

void doSomeWork(cosnt Widget& w);
// 함수 방식 캐스트를 사용, int 로부터 Widget 생성
doSomeWork(Widget(15));
// C++ 방식 캐스트를 사용, int 로부터 Widget 생성
doSomeWork(static_cast<Widget>(15));


다음 예시를 보자.

class Base { ... };
class Derived: public Base { ... };
Derived d;
// Derived* => Base* 의 암시적 변환이 이루어진다. 런타임에!
Base *pb = &d;

위에서, 객체 d 가 가질 수 있는 주소는 2개 이상이 될 수 있다.

캐스팅과 관련된, '보기엔 맞는 것 같지만 실제로는 틀린' 코드를 보자.

class Window
{
  public:
    // 기본 클래스의 onResize 구현
    virtual void onResize() { ... }
  ...
};

class SpecialWindow: public Window
{
  public:
    virtual void onResize()
    {
      // 동작이 안 됨
      static_cast<Window>(*this).onResize();
      
      // SpecialWindow 에서만 필요한 작업을 여기서 수행
      ...
    }
  ...
};

SpecialWindow 클래스에서 onResize 함수 내의 캐스팅은 제대로 이루어지지 않는다. 왜냐하면 캐스팅이 일어나면서 *this 의 기본 클래스 부분에 대한 사본이 임시적으로 만들어지게 되어 있는데, 지금의 onResize 는 바로 이 임시 객체에서 호출된 것이다! 따라서 위 코드는 현재의 객체에 대해 Window::onResize 를 호출하지 않는다.
코드를 수정하려면 캐스팅을 빼고 다음과 같이 고치면 된다.

class SpecialWindow: public Window
{
  public:
    virtual void onResize()
    {
      // *this 에서 Window::onResize 를 호출
      Window::onResize();
      // SpecialWindow 에서만 필요한 작업을 여기서 수행
      ...
    }
  ...
};


마지막으로, dynamic_cast 를 보자.
dynamic_cast 는 비용이 매우 큰 캐스팅으로, 필요한 경우가 있지만 최대한 사용을 피하는 식으로 설계 및 구현을 하는 게 좋다. 다음 예시를 보자.

class Window { ... };

class SpecialWindow: public Window
{
  public:
    void blink();
    ...
};

typedef std::vector<std::shared_ptr<Window>> VPW;
VPW winPtrs;

...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
  // 고비용 : dynamic_cast 를 사용중
  if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
    psw->blink();
}

dynamic_cast 사용을 피하기 위해, 코드를 다음과 같이 수정해볼 수 있다.

typedef std::vector<std::shared_ptr<SpecialWindow>> VPSW;
VPSW winPtrs;

...
for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
  (*iter)->blink();
}

물론 이는 SpecialWindow 말고도 Window 의 다른 파생 클래스가 있을 경우 각기 다른 파생 클래스 타입의 벡터를 만들어 주어야 한다는 단점이 존재한다. 아니면 이런 방법도 있다.

class Window 
{ 
  public:
    // '아무 동작 안하는' 가상 함수
    // 별로 좋은 아이디어는 아니다 (항목 34 참조)
    virtual void blink() {}
    ...
};

class SpecialWindow: public Window
{
  public:
    // 실제 구현
    virtual void blink() { ... }
    ...
};

typedef std::vector<std::shared_ptr<Window>> VPW;
VPW winPtrs;

...

for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
  (*iter)->blink();
}


정말 피해야 하는 설계가 하나 있는데, 바로 '폭포식(cascading) dynamic_cast' 라고 불리는 구조이다. 예시를 보자.

class Window { ... };

typedef std::vector<std::shared_ptr<Window>> VPW;
VPW winPtrs;

...

for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
  if (SpecialWindow1 *psw1 = dynamic_cast<SpecialWindow1*>(iter->get())) { ... }
  else if (SpecialWindow2 *psw2 = dynamic_cast<SpecialWindow2*>(iter->get())) { ... }
  else if (SpecialWindow3 *psw3 = dynamic_cast<SpecialWindow3*>(iter->get())) { ... }
  ...
}

위의 설계는 심지어 새로운 파생 클래스가 생길 때 마다 하나를 더 추가해야 하는, 절대 피해야 하는 설계이다!

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

핵심 :

어떤 객체의 내부요소에 대한 핸들(참조자, 포인터, 반복자) 를 반환하는 것은 되도록 피하자. 캡슐화 정도를 높이고, 상수 멤버 함수가 객체의 상수성을 유지한 채로 동작할 수 있도록 하며, 무효참조 핸들이 생기는 경우를 최소화할 수 있다.


클래스 내부의 데이터를 참조자로 반환하는 것은 주의해야 한다. 다음 예시를 보자.

clas Point
{
  public:
    Point(int x, int y);
    ...
    void setX(int newVal);
    void setY(int newVal);
    ...
};

struct RectData
{
  Point ulhc; // upper left-hand corner
  Point lrhc; // lower right-hand corner
};

class Rectangle
{
  public:
    // !참조자로 Point 객체를 반환!
    Point& upperLeft() const { return pData->ulhc; }
    Point& lowerRight() const { return pData->lrhc; }
  
  private:
    std::shared_ptr<RectData> pData;
};

...
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);

// 상수 객체인 rec 의 내부 데이터를 수정했다!
rec.upperLeft().setX(50);

Rectangle 클래스의 upperLeft 함수와 lowerRight 함수를 보면, Point 를 참조자로 반환하고 있다. 이 때문에, 상수 객체인 rec 의 데이터 멤버의 값이 수정이 된다.
ulhc 와 lrhc 는 private 으로 선언되어 있는데, 실질적으로는 참조자를 반환하는 함수로 인해 수정이 가능해지므로, public 변수나 다를 바가 없어진다. 이는 참조자 말고도 포인터나 반복자를 반환하는 경우에도 비슷한 문제가 발생할 것이다.

위 문제는 사실 다음과 같이 간단하게 해결할 수는 있다.

class Rectangle
{
  public:
    // 참조자로 Point 객체를 반환하지만, const 임
    const Point& upperLeft() const { return pData->ulhc; }
    const Point& lowerRight() const { return pData->lrhc; }
    ...
};

하지만 위처럼 핸들을 반환하는 경우는, 실제 핸들이 가리키는 녀석이 사라지는 무효참조 핸들(dangling handle) 문제가 발생할 수 있다.

class GUIObject { ... };
const Rectangle boundingBox(const GUIObject& obj);

GUIObject *pgo;
...
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());

위의 경우, boundingbox 는 Rectangle 임시 객체(temp 라고 부르자)를 만든다. 이 temp 객체는, uppderLeft 를 통해 ulhc 를 뱉어 내고, 해당 값의 주소를 pUpperLeft 에게 전달한다. 하지만 boundBox 를 호출하는 문장이 끝날 때 temp 객체는 파괴되기 때문에, pUpperLeft 는 실제 객체가 날라간 주소 값을 가지고 있게 된다!
핸들을 반환하는 멤버 함수가 필요한 경우도 있다(operator[] 연산자 등). 하지만 이런 경우는 예외적인 경우이므로, 일반적으로는 '핸들'을 반환하는 코드는 '되도록 피하도록' 하자.

0 Comments
댓글쓰기 폼