2016년 4월 21일 목요일

[Effective C++] 항목 28 : 내부에서 사용하는 객체에 대한 ‘핸들’을 반환하는 코드는 되도록 피하자

(28)      내부에서 사용하는 객체에 대한 핸들을 반환하는 코드는 되도록 피하자
class Point { // 점을 나타내는 클래스
public:
           Point(int x, int y);
           ...
           void setX(int newVal);
           void setY(int newVal);
           ...
};

struct RectData { // Rectangle에 쓰기 위한 점 데이터
           Point ulhc;         // 좌측상단(upper left-hand corner)
           Point lrhc;         // 우측상단(lower right-hand corner)
};

class Rectangle {
           ...
          
private:
           std:;tr1::shared_ptr<RectData> pData;
};

Rectangle 클래스의 사용자는영역정보를 알아내어 쓰기 위해 upperLeft(), lowerRight()가 멤버함수로 들어있다.그런데 Point가 사용자 정의 타입이고사용자 정의 타입을 전달할 때는 값에 의한 전달보다는참조에 의한 전달방식을 쓰는 편이 더 효율적이라고 …(20)
그래서 이들 두 멤버함수는 (스마트포인터로 불어둔 Point 객체에 대한 참조자를 반환하는 형태로 만들어졌습니다.

class Rectangle {
           ...
           Point& upperLeft() const { return pData->ulhc; }
           Point& lowerRight() const { return pData->lrhc; }
           ...
};

컴파일은 잘됩니다그런데 결정적으로 틀렸다.조금만 들여다 보면 자기모순적인 코드임을 알 수 있다.
우선 upperLeft(), lowerRight()가 상수멤버함수이다원래 Rectangle의 꼭짓점 정보를 알아낼 수 있는 방법만 제공하고,
Rectangle 
객체를 수정하는 일은 할 수 없도록 설계되었으니까요
.그런데 이 함수들이 반환하는 게, private 멤버인 내부 데이터에 대한 참조자 입니다.호출부에서 이 참조자를 써서 내부 데이터를 맘대로 수정해도 좋다는 뜻이 되는 거죠!

Point coord1(0,0);
Point coord2(100,100);

const Rectangle rec(coord1, coord2); // rec (0,0)~(100,100)의 영역에 있는 상수 Rectangle 객체입니다.
rec.upperLeft().setX(50); // 이제 이 rec (50,0)부터 (100,100)의 영역에 있게 됩니다.

upperLeft를 호출한 쪽은 rec의 은밀한 곳에 숨겨진 Point 데이터 멤버를 참조자로 끌어와 척척 바꿀 수 있다는 것이다.

여기서 2가지 교훈을 얻을 수 있다.첫째클래스 데이터 멤버는 아무리 숨겨봤자그 멤버의 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화 정도가 정해진다는 것이다., ulhc lrhc private으로 선언되었지만실질적으로 public 멤버입니다왜냐하면 이들의 참조자를 반환하는 upperLeft, lowerRight 함수가 public 멤버함수이기 때문입니다.
둘째어떤 객체에서 호출한 상수 멤버 함수의 참조자 반환 값의 실제 데이터가 그 객체의 바깥에 저장되어 있다면이 함수의 호출부에서 그 데이터의 수정이 가능하다는 점입니다.
(
사실 이점은 비트수준의 상수성의 한계가 가진 부수적 성질에 불과합니다 (3)).

만약에 이들이 포인터나 반복자를 반환하도록 되어 있다고 해도마찬가지 이유로 인해마찬가지 문제가 생깁니다참조자포인터 및 반복자는 어쨌든 모두 핸들(handle, 다른 객체에 손을 댈 수 있게 하는 매개자)이고어떤 객체의 내부요소에 대한 핸들을 반환하게 만들면언제든지 그 객체의 캡슐화를 무너뜨리는 위험을 무릅쓸 수밖에 없습니다.

어떤 객체의 내부요소(internals)’라고 하면흔히들 데이터 멤버만 생각하는데일반적인 수단으로 접근이 불가능한 (protected, private으로 선언된멤버함수도 객체의 내부요소에 들어갑니다.

upperLeft, lowerRight가 가진 문제 2개는이렇게 하면 간단히 해결됩니다.반환타입에 const 키워드만 붙여주세요.

class Rectangle {
           ...
           const Point& upperLeft() const { return pData->ulhc; }
           const Point& lowerRight() const { return pData->lrhc; }
           ...
};

이렇게 설계하면사용자는 사각형을 정의하는 꼭짓점 쌍을 읽을 수는 있지만쓸 수는 없게 됩니다
말하자면 upperLeft, lowerRight const를 붙여 선언한 게 이젠 거짓이 아니라는 것이죠.호출부에서 객체의 상태를 바꾸지 못하도록 컴파일러 수준에서 막고 있거든요.
그리고 캡슐화 문제인데사용자들이 Rectangle을 구성하는 Point 내부를 볼 수 있도록 만든 것은처음부터 알고 시작한 설계이기 때문에이 부분은 의도적인 캡슐화 완화라고 할 수 있겠습니다.이보다 더 중요한 부분은 느슨하게 만든 데에도 제한을 두었다는 것입니다읽기 접근만 주어지고쓰기 접근은 여전히 금지죠.

내부데이터에 대한 핸들을 반환하는 부분.
가장 큰 문제가 무효참조 핸들(dangling handle) 로서핸들이 있기는 하지만그 핸들을 따라 갔을 때실제 객체의 데이터가 없는 경우 입니다.핸들이 물고 잇는 객체가 기약도 없이 사라지는 현상은 함수가 객체를 값으로 반환할 경우에 가장 흔하게 발생됩니다.

class GUIObject {...};

const Rectangle boundingBox(const GUIObject& obj); // Rectangle 객체를 값으로 반환합니다반환타입이 const인 이유는(3)

이 상태에서 어떤 사용자가 이 함수를 사용한다.

GUIObject* pgo; // pgo를 써서 임의의 GUIObject를 가리키도록 한다.
...
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft()); // pgo가 가리키는 GUIObject의 사각 테두리 영역으로부터 좌측 상단 꼭짓점의 포인터를 얻습니다.

boundingBox를 호출하면 Rectangle 임시객체가 새로 만들어집니다.이 임시 객체의 upperLeft가 호출되면이 호출로 인해 임시객체의 내부 데이터 Point 객체 중 하나에 대한 참조자가 나옵니다마지막으로 이 참조자에 & 연산자를 건 결과 값(주소) pUpperLeft 포인터에 대입되는 것이죠.
그러나 이 문장이 끝나면, boundingBox의 반환값(임시객체)이 소멸되므로그 안의 Point 객체도 덩달아 사라집니다.
결과적으로 이 문장은 pUpperLeft에게 객체를 달아 줬다가 주소 값만 남기고 몽땅 빼앗아 간 것입니다.

객체의 내부에 대한 핸들을 반환하는 함수는 위험합니다.핸들이 무엇인가는 상관없습니다포인터참조자반복자이든 마찬가지입니다.핸들에 const 유무와도 상관없습니다핸들을 반환하는 함수가 상수 멤버 여부도 상관없습니다핸들을 반환하는 함수라는 사실그것 빼고는 아무것도 중요치 않습니다.일단 바깥으로 떨어져 나간 핸들은 그 핸들이 참조하는 객체보다 더 오래 살 위험이 있기 때문입니다.

그렇다고 핸들을 반환하는 함수를 절대로 두지 말라는 뜻은 아닙니다피하자는 것이죠.필요할 때도 있습니다. Operator[] 연산자는 string, vector 등의 클래스에서 개개의 원소를 참조할 수 있게 만드는 용도로 제공되고 있는데실제로 이 연산자는 내부적으로 해당 컨테이너에 들어 있는 개개의 원소 데이터에 대한 참조자를 반환하는 식으로 동작합니다(3)물론 이 원소 데이터는 컨테이너가 사라질 때 같이 사라지는 데이터이죠.하지만 이런 함수는 예외적인 것입니다일반적인 규칙이 아니라고요.

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




댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...