28: 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자
class Point {
public:
Point(int x, int y);
void setX(int newVal);
void setY(int newVal);
};
struct RectData {
Point ulhc;
Point lrhc;
};
class Rectangle {
public:
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
private:
tr1::shared_ptr<RectData> pData;
};
위의 코드는 컴파일은 잘 된다. 하지만 결정적으로 틀렸다. 상수 멤버 함수은 upperLeft와 lowerRight가 Point에 대한 참조자를 반환하는 형태로 만들어졌다. 원래 Rectangle의 꼭짓점 정보를 알아낼 수 있는 방법만 사용자에게 제공하고, Rectangle 객체를 수정하는 일은 할 수 없도록 설계되었다. 하지만 호출부에서 이 참조자를 써서 내부 데이터를 맘대로 수정할 수 있다.
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2); // (0, 0)부터 (100, 100)의 영역에
// 있는 상수 Rectangle 객체
rec.upperLeft().setX(50) // (50, 0)부터 (100, 100)으로 변경
여기서 두 가지 교훈을 얻을 수 있다. 첫째, 클래스 데이터 멤버는 아무리 숨겨봤자 그 멤버의 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화 정도가 정해진다는 점이다. 위의 ulhc와 lrhc는 private 멤버이지만 실질적으로 public이다. 왜냐하마녀 이들의 참조자를 반환하는 upperLeft 및 lowerRight 함수가 public 멤버 함수이기 때문이다. 둘째, 어떤 객체에서 호출한 상수 멤버 함수의 참조자 반환 값이 실제 데이터가 그 객체의 바깥에 저장돠어 있다면, 이 함수의 호출부에서 그 데이터의 수정이 가능하다는 점이다.
참조자, 포인터 및 반복자는 모두 핸들(handle, 다른 객체에 손을 댈 수 있게 하는 매개자)이고, 어떤 객체의 내부요소에 대한 핸들을 반환하게 만들면 언제든지 그 객체의 캡슐화를 무너뜨리는 위험을 무릅쓸 수밖에 없다.
멤버 함수도 객체의 내부요소에 들어간다. 이들에 대한 핸들도 반환하지 말아야 한다. 즉, 외부 공개가 차단된 멤버 함수에 대해, 이들의 포인터를 반환하는 멤버 함수를 만드는 일이 절대로 없어야 한다는 말이다.
다시 앞으로 돌아가서 반환 타입에 const 키워드를 붙이는 것으로 문제를 해결할 수 있다.
class Rectangle {
public:
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
private:
tr1::shared_ptr<RectData> pData;
};
이렇게 설계한면, 읽을 수는 있지만 쓸 수는 없게 된다.
아직 남아있는 문제는 upperLeft와 lowerRight 함수의 내부 데이터에 대한 핸들을 반환하고 있는 부분이다. 가장 큰 문제가 뮤효참조 핸들(dangling handle)로서, 핸들이 있기는 하지만 그 핸들을 따라갔을 때 실제 객체의 데이터가 없는 것이다. 함수가 객체를 값으로 반환할 경우에 가장 흔하게 발생한다.
class GUIObject {};
const Rectangle boundingBox(const GUIObject& obj);
GUIObject* pgo;
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
가장 마지막 문장에서 boundingBox를 호출하면 Rectangle 임시 객체(temp)가 만들어진다. 이 temp에 대해 upperLeft가 호출되면 이 호출로 인해 temp의 내부 데이터에 대한 참조자가 나온다. 하지만 이 문장이 끝날 무렵, boundingBox 함수의 반환 값(임시 객체인 temp)이 소멸된다. temp가 소멸되면 그 안에 들어 있는 Point 객체들도 덩달아 없어진다. 그럼 pUpperLeft 포인터가 가리키는 객체는 이제 날아가고 없게 된다.
일단 바깥으로 떨어져 나간 핸들은 그 핸들이 참조하는 객체보다 더 오래 살 위험이 있다.
이것만은 잊지 말자!
- 어떤 객체의 내부요소에 대한 핸들(참조자, 포인터, 반복자)을 반환하는 것은 되도록 피하자. 캡슐화 정도를 높이고, 상수 멤버 함수가 객체의 상수성을 유지한 채로 동작할 수 있도록 하며, 무효참조 핸들이 생기는 경우를 최소화할 수 있다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 30: 인라인 함수는 미주알고주알 따져서 이해해 두자 (0) | 2023.10.29 |
---|---|
[Effective C++] 29: 예외 안정성이 확보되는 그날 위해 싸우고 또 싸우자! (0) | 2023.10.28 |
[Effective C++] 27: 캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2023.10.26 |
[Effective C++] 26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2023.10.23 |
[Effective C++] 25: 예외를 던지는 swap에 대한 지원도 생각해 보자 (1) | 2023.10.22 |