본문 바로가기
서적 정리/Effective C++

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

by 민돌이2 2021. 12. 19.

클래스를 설계할 때 메모리 부담을 최소화 하고 싶다고 생각하고 고민한다.

 

사각형은 꼭짓점 두 개로 표현할 수 있다. 꼭짓점 두 개를 갖는 사각형 클래스를 구현한다고 생각했을 때 지금까지 책에서 봤던 것들을 기반으로 클래스를 설계 한다면

class Point
{
public:
	Point(int x, int y)
		: x(x), y(y)
	{ }

	void SetX(int newVal) { x = newVal; }
	void SetY(int newVal) { y = newVal; }

private:
	int x, y;
};

struct RectData
{
	Point ulp; //upper left point 좌측 상단
	Point lrp; //lower right point 우측 하단
};

class Rectangle
{
public:
	...

	Point& LowerRight() const { return pData->lrp; } //좌측 상단 반환
	Point& UpperLeft() const { return pData->ulp; } //우측 하단 반환

private:
	shared_ptr<RectData> pData;
};

사용자 정의 타입을 반환할 때 값에 의한 전달보다 참조에 의한 전달방식이 효율적이기에 Point& 참조자로 반환하는 함수를 만들었다.

컴파일도 잘되고 문제가 없어보이지만 LowerRight 함수와 UpperLeft 함수는 상수 멤버 함수이므로 자기모순적인 코드이다. 이 두 개의 함수는  private 멤버 변수에 대한 참조자를 반환하는데 호출부에서 이 참조라를 사용하여 멤버 데이터를 맘대로 수정이 가능하다.

Point p1(0, 0);
Point p2(100, 100);

const Rectangle rec(p1, p2);

rec.UpperLeft().SetX(50); //좌측 상단의 X값을 수정

이 코드를 보고 어이가 없었다. 이따구로도 사용할수도 있구나 싶었다. Rectangle 객체를 상수로 선언함으로써 꼭지점을 변경하지 못하게 바꿔놨고 출력만 가능하게 구현해놨더니 맘대로 바꿀 가능성이 있었다는 것이다.

어떤 객체에서 호출한 상수 멤버 함수의 참조자 반환 값의 실제 데이터가 그 객체의 바깥에 저장되어 있다면, 이 함수의 호출부에서 그 데이터의 수정이 가능하다는걸 알 수 있다.

클래스 데이터 멤버는 아무리 숨겨봤자 그 멤버의 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화의 정도가 정해진다. 캡슐화 한다고 해봤자 멤버의 참조자를 반환하는 함수들로 인해 캡슐화의 목적을 잃을 수 있다는 것이다.

private 혹은 protected 멤버로 선언된 데이터라 할지라도 이들의 포인터를 반환하는 멤버 함수를 만들지 말자.

 

간단하게 반환 참조자에 const를 붙여주면 해결은 할 수있다.

const Point& LowerRight() const { return pData->lrp; } //좌측 상단 반환
const Point& UpperLeft() const { return pData->ulp; } //우측 하단 반환

 

하지만 무효참조 핸들(dangling handle)이라는 큰 문제는 아직 남아있다.

핸들을 따라갔을 때 실체 객체의 데이터가 없을 수 있다는 것이다. 핸들이 물고 있는 객체가 기약도 없이 어디론가 증발하는 현상은 함수가 객체를 값으로 반환할 경우 가장 흔하게 발생한다.

class Point { ... };
struct RectData { ... };
class Rectangle { ... };
class GUIObject { ... }; 

const Rectangle BoundingBox(const GUIObject& obj); //GUIObject을 Rectangle 객체로 반환

int main()
{
	GUIObject* obj;
	const Point* ulp = &(bounding(*obj).UpperLeft()); //obj가 갖는 좌측 상단 꼭지점의 포인터 반환

	system("Pause");
	return 0;
}

BoundingBox 함수에서 Rectangle 임시 객체가 생성 되고 이 객체를 temp라고 했을 때, 이 temp가 반환되고 UpperLeft 함수가 호출되어 좌측 상단 꼭지점의 참조자를 반환할 것이다.

여기서 생각을 해보자. temp는 BoundingBox 함수에서 지역 변수였다. 즉, ulp는 소멸될 주소를 가리킨다는 것이다.

 

이번 챕터를 정리하자면 핸들을 반환하는 멤버 함수를 절대로 만들지 말자가 아닌 피하자이다.

무효참조 핸들은 어떻게 방법이 없는것 같다. 사실 지금까지 포트폴리오 제작을 하면서 핸들 즉, 내가 만든 클래스를 통째로 반환하는 함수는 무진장 많이 만들었고, 문제도 없었다. 내가 쓸 함수니 저런 오류사항은 일어날일이 없었기 때문이다.

생각해보면 operator[] 연산자는 STL의 string이나 vector 등의 클래스에서 개개의 원소의 참조자를 반환하는 식으로 동작한다. 이 원소 데이터는 컨테이너가 사라질 때 같이 사라지는 데이터이지만 예외적이니 물고늘어지지 말자.

 

이것만은 잊지 말자

어떤 객체의 내부요소에 대한 핸들(참조자, 포인터, 반복자)을 반환하는 것은 되도록 피하자.

캡슐화 정도를 높이고, 상수 멤버 함수가 객체의 상수성을 유지한 채로 동작하도록 하고, 무효참조 핸들이 생기는 경우를 최소화 할수 있다.

728x90

댓글