[Effective C++] 28. 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자
예시로 사각형 클래스로부터 시작해보자.
메모리 효율을 높이기 위해 꼭짓점(Point)을 별도의 구조체(RectData)에 넣어 사각형 클래스(Rectangle)가 이를 가리키도록 해보자.
class Point { public: Point(int x, int y); void setX(int newVal); void setY(int newVal); }; struct RectData { Point ulhc; // 좌측상단 Point lrhc; // 우측하단 }; class Rectangle { private: std::tr1::shared_ptr<RectData> pData; public: // 아래 두행은 전부 상수멤버함수임을 기억하자. Point & upperLeft() const { return pData->ulhc; } // 좌측상단 값 get Point & lowerRight() const { return pData->lrhc; } // 우측하단 값 get };
위의 코드를 보면 상수 멤버 함수로 upperLeft(), lowerRight()를 선언하였다. 상수 객체함수를 통해 호출객체가 수정되지 않게하기위함않게 하기 위함( = 상수성을 유지하기 위해)이 그 이유이다.
하지만 이 함수들이 반환하는 게 private 멤버인 내부 데이터에 대한 참조자여서 아래와 같이 사용했을 때 상수 객체가 참조하는 데이터가 수정돼버린다.
Point coord1(0,0); Point coord2(100,0); const Rectangle rec(coord1, coord2); rec.upperLeft().setX(50); // 수정됨
여기서 2가지 교훈을 얻을 수 있는데,
1. 데이터 멤버(ex.ulhc, lrhc)를 private로 선언한다 하더라도 그 멤버의 참조자를 반환하는 함수(ex. upperLeft(), lowerRight()가 public => 데이터 멤버의 접근도가 private이라도 해당 함수로 인해 접근도가 변경됨)들의 접근도에 따라 그 캡슐화 정도가 정해진다.
2. 어떤 객체에서 호출한 상수 멤버 함수(ex. Point& upperLeft() const )의 참조자반환 값의 실제 데이터가 해당 객체의 바깥에저장되어 있다면 이 함수의 호출부에서 그 데이터가 수정이 가능하다.
이러한 교훈을 통해 '핸들(다른 객체에 손댈 수 있는 매개자 : 참조자, 포인터, 반복자)을 반환하는 코드는 최대한 피하자.'는 것을 알 수 있다.
해당 문제를 해결하는 방법으로는 상수 멤버 함수 upperLeft()와 lowerRight()의 반환 타입에 const를 붙여 상태 변경을 금지시키는 방법이 있다.
class Rectangle { public: const Point& upperLeft() const{ return pData->ulhc; } const Point& lowerRight() const{ return pData->lrhc; } };
다음과 같이 const를 앞에 붙여주면 핸들을 반환해도 그 값을 변경할 수는 없기 때문에 해결된다. 또한 캡슐화 문제라면 이 클래스는 의도적으로 Point를 들여다보도록 설계한 것이기 때문에 의도적인 캡슐화 완화라고 볼 수 있다.
하지만 완벽하게 해결한 것은 아니다. 또 다른 문제가 발생할 수 있기 때문이다.
무효 참조 핸들
무효 참조 핸들이란 그 핸들이 가리키는 데이터를 따라갔을 때 해당 데이터가 없는 것으로 아래의 예시를 보자.
class GUIObject{}; const Rectangle boundingBox(const GUIObject& obj); GUIObject *pgo; const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
GUI 객체의 사각 테두리 영역을 Rectangle 객체로 반환하는 함수가 있다고 하자. boundingBox를 호출하면 임시 객체가 생성되는데 이 임시 객체에 대해 upperLeft가 호출되며 두 Point 객체 중 하나에 대한 참조자가 나온다. 그리고 그 주소 값이 pUpperLeft에 반환되는 형식이다. 그러나 이 문장이 끝날 무렵 임시 객체는 소멸되기 때문에 pUpperLeft가 가리키는 객체는 날아가고 없게 된다.
따라서 "핸들을 반환하는 것"을 반드시 되도록 피해야 하며, 어떻게든 문제가 발생할 수 있는 여지가 있기 때문에 operator [] 연산자와 같이 특별하게 필요한 경우가 아니라면 반드시 피하도록 하자.
요약
- 어떤 객체의 내부 요소에 대한 핸들(참조자, 포인터, 반복자)을 반환하는 것은 되도록 피해야 한다. 캡슐화 정도를 높이고, 상수 멤버 함수가 객체의 상수성을 유지한 채로 동작할 수 있도록 하며, 무효 참조 핸들이 생기는 경우를 최소화할 수 있다.
'Books > Effective C++' 카테고리의 다른 글
[Effective C++] 30. 인라인 함수는 미주알고주알 따져서 이해해 두자 (0) | 2022.06.19 |
---|---|
[Effective C++] 29. 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자! (0) | 2022.06.19 |
[Effective C++] 27. 캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2022.06.18 |
[Effective C++] 26. 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2022.06.18 |
[Effective C++] 25. 예외를 던지지 않는 swap에 대한 지원도 생각해 보자 (0) | 2022.06.12 |
댓글
이 글 공유하기
다른 글
-
[Effective C++] 30. 인라인 함수는 미주알고주알 따져서 이해해 두자
[Effective C++] 30. 인라인 함수는 미주알고주알 따져서 이해해 두자
2022.06.19인라인 함수의 장단점 인라인 함수는 함수 호출문을 그 함수의 본문으로 바꿔치기하는 것이다. 따라서 목적 코드의 크기가 커질 염려가 있고 메모리가 제한된 컴퓨터에서 아무 생각 없이 인라인을 남발했다가는 프로그램 크기가 그 기계에서 쓸 수 있는 공간을 넘어버릴 수도 있다. 가상 메모리를 쓰는 환경일지라도 인라인 함수로 인해 부풀려진 코드는 성능의 걸림돌이 되기 쉽다. 페이징 횟수가 늘어나고, 명령어 캐시 적중률이 떨어질 가능성도 높아진다. 그러나 본문 길이가 굉장히 짧은 인라인 함수를 사용하면, 함수 본문에 대해 만들어지는 코드의 크기가 함수 호출문에 대해 만들어지는 코드보다 작아질 수도 있다. 이렇게 되면 목적 코드의 크기도 작아지고 명령어 캐시 적중률도 높아진다. 인라인 함수에 대한 오해 인라인 함수의 … -
[Effective C++] 29. 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!
[Effective C++] 29. 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!
2022.06.19 -
[Effective C++] 27. 캐스팅은 절약, 또 절약! 잊지 말자
[Effective C++] 27. 캐스팅은 절약, 또 절약! 잊지 말자
2022.06.18 -
[Effective C++] 26. 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자
[Effective C++] 26. 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자
2022.06.18생성자 혹은 소멸자를 끌고 다니는 타입으로 변수를 정의하면 반드시 물게 되는 비용이 두 개 있다. 프로그램 제어 흐름이 변수의 정의에 닿을 때 생성자가 호출되는 비용, 변수가 유효 범위를 벗어날 때 소멸자가 호출되는 비용이다. 이러한 비용은 변수가 정의만 돼도 부과된다. 예제를 보자. std::string encryptPassword(const std::string& password) { using namespace std; string encrypted; if(password.length() < MinimumPasswordLength) { throw logic_error("Password is too short"); } return encrypted; } encrypted를 안 쓴다고 할 순 없지만, …
댓글을 사용할 수 없습니다.