이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 191p ~ 200p
이펙티브 C++(191p ~ 200p)
요약
항목 27: 캐스팅은 절약, 또 절약! 잊지 말자
- 캐스팅이 들어가면, 보기엔 맞는 것 같지만 실제로는 틀린 코드를 쓰고도 모르는 경우가 많아진다.
// 기본 클래스
class Window {
public:
virtual void onResize() {}
}
// 파생 클래스
class SpecialWindow: public Window {
// 파생 클래스의 onResize에서 *this를 Window로 캐스팅하고, 그것에 대해 onResize 호출
// 동작은 안 됨
virtual void onResize() {
static_cast<Window>(*this).onResize();
}
}
*this
를 캐스팅하면 기본 클래스 부분에 대한 사본이 임시적으로 만들어지게 되어 있는데, 지금의 onResize는 이렇게 생성된 임시 객체에서 호출된다.- 따라서 위의 코드는 현재의 객체에 대해 onResize를 호출하지 않고 지나간다.
- 이 문제를 해결하려면 일단 캐스팅을 빼야된다.
// 파생 클래스
class SpecialWindow: public Window {
virtual void onResize() {
Window::onResize();
}
}
dynamic_cast
는 안전한 다운 캐스팅에 사용되는 연산자이지만, 구현환경에 따라 클래스 이름에 대한 문자열 비교 연산에 기반을 두어 느린 경우가 있다.dynamic_cast
연산자는 파생 클래스 객체임이 분명한 상황에서는 유용하게 사용할 수 있는데, 비용 문제를 해결하기 위한 방법은 다음과 같다.- 파생 클래스 객체에 대한 포인터를 컨테이너에 담아둠으로써 각 객체를 기본 클래스 인터페이스를 통해 조작할 필요를 아예 없애 버린다.
class Window {};
class SpecialWindow: public Window {
public:
void blink();
}
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
// dynamic 캐스트를 사용함
for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
psw->blink();
}
// dynamic 캐스트 없음
// 모든 Window 파생 클래스에 대한 포인터를 담을 수는 없음
// 다른 포인터를 담으려면 타입 안전성을 갖춘 컨테이너 여러 개가 필요
typedef std::vector<std::tr1::shared_ptr<SpecialWindow>> VPSW;
VPSW winPtrs;
for(VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
(*iter)->blink();
}
- 다른 방법은 Window에서 뻗어 나온 자손들을 전부 기본 클래스 인터페이스를 통해 조작할 수 있게 원하는 조작을 가상 함수 집합으로 정리하는 것이다.
class Window {
public:
virtual void blink() {}
};
class SpecialWindow: public Window {
public:
virtual void blink() {}
}
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
// dynamic 캐스트를 사용안하고 virtual 함수를 이용함
for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
(*iter)->blink();
}
- 폭포식 dynamic_cast 구조는 반드시 피해야 한다.
- 캐스팅을 해야 하는 코드가 있다면, 그 코드를 내부 함수로 몰아 놓고 그 안에서만 캐스팅이 일어나도록 해야 한다.
항목 28: 내부에서 사용하는 객체에 대한 핸들을 반환하는 코드는 되도록 피하자
- 다음과 같이 사각형(Rectangle)을 나타내는 클래스를 정의해보자.
- 사각형은 좌상단과 우하단 꼭짓점 두 개로 나타낼 수 있기 때문에, 사각형의 객체를 사용할 때 메모리 부담을 최대한 줄이고자 사각형의 영역을 정의하는 두 점을 관리하는 별도의 구조체를 만들어 Rectangle 클래스에서 가리키게 하였다.
class 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 {
private:
std::tr1::shared_ptr<RectData> pData;
};
- 해당 클래스의 사용자는 분명히 영역정보를 알아내어 쓸 때가 있을 것이기 때문에, upperLeft, lowerRight 함수를 제공해야 할 것이다.
- 사용자 정의 타입은 참조에 의한 전달이 낫기 때문에 다음과 같이 정의한다.
class Rectangle {
public:
Point& upperLeft() const { return pData->lrhc; }
Point& lowerRight() const { return pData->lrhc; }
private:
std::tr1::shared_ptr<RectData> pData;
};
- 컴파일에는 문제가 없지만 설계상 틀린 부분이 있다.
upperLeft
함수와lowerRight
함수는 상수 멤버 함수이다. 즉, 해당 함수를 통해 사각형의 꼭짓점 정보는 알아낼 수 있지만, 해당 정보를 수정할 수는 없다.- 그러나 반환값은 private 멤버인 내부 데이터의 참조자이기 때문에 외부에서 값을 수정할 수 있게 된다.
Point coord1(0, 0);
Point coord2(100, 100);
// 상수 Rectangle 객체 (0, 0) ~ (100, 100) 영역을 가짐
const Rectangle rec(coord1, coord2);
// 상수 Rectangle 객체의 영역이 (50, 0) ~ (100, 100)으로 수정됨
rec.upperLeft().setX(50);
- 이런 상황을 통해 다음과 같은 교훈을 얻을 수 있다.
- 클래스 데이터 멤버는 아무리 숨겨 봤자 그 멤버의 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화 정도가 정해진다.
- 참조자, 포인터, 반복자는 모두 핸들(handle, 다른 객체에 손을 댈 수 있게 하는 매개자)이고, 핸들을 반환하면 캡슐화를 무너뜨릴 가능성이 높아진다.
- 이는 데이터 멤버 뿐만 아니라 멤버 함수에 대해서도 동일하게 적용된다.
- 클래스 데이터 멤버는 아무리 숨겨 봤자 그 멤버의 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화 정도가 정해진다.
- 반환되는 참조자도 상수로 만들어 주는 것으로 일단 이 문제를 해결할 수 있다.
- 이렇게 되면 의도적으로 Rectangle 클래스의 사용자는 각 꼭짓점 정보는 읽을 수 있지만, 꼭짓점에 대한 쓰기 작업은 막을 수 있다.
class Rectangle {
public:
const Point& upperLeft() const { return pData->lrhc; }
const Point& lowerRight() const { return pData->lrhc; }
private:
std::tr1::shared_ptr<RectData> pData;
};
- 아직도 한 가지 문제가 남아있다. 바로 댕글링 핸들 문제이다.(dangling handle)
- 핸들이 있기는 한데, 그 핸들을 따라갔을 때 실제 객체의 데이터가 없는 현상이다.
- 핸들이 물고 있는 객체가 증발하는 현상은 함수가 객체를 값으로 반환할 경우에 가장 흔하게 발생한다.
class GUIObject {};
// Rectangle 객체를 값으로 반환
const Rectangle boundingBox(const GUIObject& obj);
GUIObject* pgo;
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
- 위의 코드에서 마지막 문장을 실행하면
boundingBox
함수를 통해 Rectangle 임시 객체가 새로 만들어진다.- 그리고 나서 해당 임시 객체의
upperLeft
가 호출되고,Point
객체에 대한 참조자가 반환된다. - 마지막으로 & 연산자를 사용하여 참조자의 주소를
*pUpperLeft
에 대입하게 된다. - 문제는 이 문장이 끝나면서 함수의
boundingBox
의 Rectangle 임시 객체가 소멸된다.(포인터도 동시에 소멸) *pUpperLeft
에는 가리키는 객체가 없어진 것이다.
- 그리고 나서 해당 임시 객체의
메모
항목 27: 캐스팅은 절약, 또 절약! 잊지 말자
- 다른 방법이 가능하다면 캐스팅은 피하자. 특히 수행 성능에 민감한 코드에서
dynamic_cast
는 몇 번이고 다시 생각해야 한다. 설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해보자. - 캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있도록 하자. 이렇게 하면 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고 이 함수를 호출할 수 있게 된다.
- 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호하자. 발견하기도 쉽고, 설계자가 어떤 역할을 의도했는지가 더 자세히 드러난다.
항목 28: 내부에서 사용하는 객체에 대한 핸들을 반환하는 코드는 되도록 피하자
- 어떤 객체의 내부요소에 대한 핸들(참조자, 포인터, 반복자)을 반환하는 것은 되도록 피하자.
- 캡슐화 정도를 높이고, 상수 멤버 함수가 객체의 상수성을 유지한 채로 동작할 수 있도록 하며, 무효참조 핸들이 생기는 경우를 최소화 할 수 있다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(210p ~ 220p) (2) | 2024.01.26 |
---|---|
이펙티브 C++(201p ~ 210p) (0) | 2024.01.25 |
이펙티브 C++(183p ~ 190p) (1) | 2024.01.23 |
이펙티브 C++(169p ~ 182p) (2) | 2024.01.22 |
이펙티브 C++(160p ~169p) (1) | 2024.01.21 |