2016년 4월 21일 목요일

[Effective C++] 항목 29 : 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!

(29)      예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!
GUI 메뉴 구현을 위한 클래스
class PrettyMenu {
public:
           ...
           void changeBackground(std::istream& imgSrc); // 배경그림을 바꾸는 멤버함수
           ...
          
private:
           Mutex mutex; // 이 객체 하나를 위한 뮤텍스
          
           Image* bgImage; // 현재의 배경그림
           int imageChaged; // 배경그림이 바뀐횟수
};

PrettyMenu changeBackground의 구현
void Pretty::changeBackground(std::istream& imgSrc)
{
           lock(&mutex); // 뮤텍스 획득(14)
          
           delete bgImage; // 이전의 배경그림을 없앱니다.
           ++imageChanges; // 그림 변경 횟수를 갱신합니다.
           bgImage = new Image(imgSrc); // 새 배경그림을 깔아 놓습니다.
          
           unlock(&mutex); / 뮤텍스 해제.
}

예외 안전성이라는 측면에서 볼 때이 함수는 이보다 더 나쁠 수는 없다”.일반적으로 예외 안전성을 확보하려면 2가지의 요구사항을 맞추어야 하는데, 이 함수는 어느 요구사항에도 맞지 않는위험천만의 함수입니다.

예외 안전성을 가진 함수라면예외가 발생할 때 이렇게 동작해야 합니다.
l  자원이 새도록 만들지 않습니다.위의 코드는 자원이 샙니다.왜냐하면 “new Image(imgSrc)” 표현식에서 예외를 던지면,
unlock 
함수가 실행되지 않게 되어뮤텍스가 계속 잡힌 상태로 남기 때문입니다.
l  자료구조가 더럽혀지는 것을 허용하지 않습니다.1. “new Image(imgSrc)”가 예외를 던지면 bgImage가 가리키는 객체는 이미 삭제된 후입니다.
2. 
새 그림이 제대로 깔린 게 아닌데도, imageChanges 변수는 이미 증가되었습니다.

자원 누출 문제 해결방법은객체를 써서 자원 관리를 전담케 하는 방법(13)뮤텍스를 적절한 시점에 해제하는 방법을 구현한 Lock 클래스(14)를 
그대로 따라하면 마무리 된다.

void Pretty::changeBackground(std::istream& imgSrc)
{
           Lock m1(&mutex); // 항목(14): 뮤텍스를 대신 획득하고이것이 필요 없어질 시점에 바로 해제해 주는 객체
          
           delete bgImage;
           ++imageChanges;
           bgImage = new Image(imgSrc);
          
           unlock(&mutex); / 뮤텍스 해제.
}
Lock 등의 자원관리 전담 클래스를 쓰면 가장 좋은 점 중 하나는함수의 코드 길이가 짧아진다는 것입니다. Unlock을 호출할 필요가 없습니다.

자료구조 오염 문제.
예외 안전선을 갖춘 함수는 다음 3가지 보장(guarantee) 중 하나를 제공해야 합니다..
l  기본적인 보장(basic guarantee)함수 동작 중에 예외가 발생하면실행중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장입니다.어떤 객체나 자료구조도 더럽혀지지 않으며모든 객체의 상태는 내부적으로 일관성을 유지하고 있습니다. (모든 클래스 불변속성이 만족된 상태입니다).하지만 프로그램의 상태가 정확히 어떠한지는 예측이 안될 수도 있습니다.예를 들어, changeBackground 가 동작하다가예외가 발생했을 때 PrettyMenu 객체는 바로 이전의 배경그림을 그대로 계속 그릴 수도 있고아니면 처음부터 마련해 둔 기본 배경그림을 사용할 수도 있을 것입니다.이 부분은 전적으로 함수를 만든 사람에 달려 있지요.하지만 사용자 쪽에서는 어떻게 될지 예측할 수 없습니다 (알아내려면현재의 배경그림이 무엇인지를 알려 주는 다른 멤버 함수를 호출하든지 해야겠지요).
l  강력한 보장(string guarantee): 함수 동작 중에 예외가 발생하면프로그램의 상태를 절대로 변경하지 않겠다는 보장입니다이런 함수를 호출하는 것은 원자적(atomic) 동작이라고 할 수 있습니다호출이 성공하면 (예외가 발생하지 않으면마무리까지 완벽하게 성공하고호출이 실패하면 함수 호출이 없었던 것처럼 프로그램의 상태가 되돌아간다는 면에서 말이죠.
쓰기 편한가의 측면에서 보면 강력한 보장을 제공하는 함수가 기본 보장을 제공하는 함수보다 더 쉽습니다예측할 수 있는 프로그램의 상태가 2개 밖에 안 되기 때문입니다그러니까 함수가 성공적으로 실행을 마친 후의 상태,아니면 함수가 호출될 때의 상태만 존재하는 거죠이와 대조적으로 함수가 기본 보장을 제공하는 경우에는예외 발생 시에 프로그램이 있을 수 있는 상태가 그냥 유효하기만 하면어떤 상태도 될 수 있습니다.
l  예외불가 보장(nothrow guarantee)예외를 절대로 던지지 않겠다는 보장입니다. 약속한 동작은 언제나 끝까지 완수하는 함수라는 뜻이죠기본 제공 타입(int, 포인터 등)에 대한 모든 연산은 예외를 던지지 않게 되어 있습니다 (예외불가 보장이 제공됩니다). 예외에 안전한 코드를 만들기 위한 가장 기본적이며 핵심적인 요소가 아닐까 싶네요.어떤 예외도 던지지 않게끔 예외 지정이 된 함수는예외불가 보장을 제공한다고 생각해도 일견 맞을 것 같지만잘못 생각하신 겁니다.
Int doSomething() throw(); // 비어 있는 예외 지정.
위의 함수 선언이 전하는 메시지는 doSomething이 절대로 예외를 던지지 않겠다는 말이 아닙니다만약 doSomething에서 예외가 발생되면 매우 심각한 에러가 생긴 것으로 판단되므로지정되지 않은 예외가 발생했을 경우에 실행되는 처리자는 unexpected 함수가 호출되어야 한다는 뜻입니다사실 doSomething은 어떤 예외 안전성 보장도 제공하지 않을 수도 있습니다함수 선언문에는(예외 지정이 붙어 있으면 이것도 포함됩니다해당 함수가 맞는지,이식성이 있는지아니면 효율적인지 알려 주는 기능 같은 것이 없습니다예외 안전성 보장을 제공하는지도 당연히 알려 주지 않습니다함수가 어떤 특성을 갖느냐 하는 부분은 구현이 결정하는 것입니다. ‘선언은 그냥 선거공약 같은 거라고요.

앞에서 말했지만예외 안전성을 갖춘 함수는 위의 세 가지 보장 중 하나를 반드시 제공해야 합니다아무 보장도 제공하지 않으면 예외에 안전한 함수가 아닙니다따라서 여러분이 선택해야 하는 것은 어떤 보장을 제공할 것인가이겠습니다.예외 안전성이 없는 재래식(legacy) 코드를 사용해서 작업할 때를 제외하면(이 부분에 대한 이야기는 이번 항목의 뒷부분에서 따로 설명하겠습니다),

위의 3가지 보장 중에 하나를 고르라면 아무래도 실용성이 있는 강력한 보장이 괜찮아 보일 것입니다예외 안정성의 관점에서 보면 예외불가 보장이 가장 훌륭하겠지만예외를 던지는 함수를 호출하지 않고 C++ C 부분으로부터 벗어나오기란 힘들거든요.일단 동적 할당 메모리를 사용하는 쪽(STL 컨테이너가 실제로 그렇습니다)만 보아도요청에 맞는 메모리를 확보할 수 없으면 bad_alloc 예외를 던지도록 구현되어 있지 않습니까(항목49).뭐 할 수 있으면 예외불가 보장을 제공하세요하지만 현실적으로는 대부분의 함수에 있어서 기본적인 보장과 강력한 보장 중 하나를 고르게 됩니다.

changeBackground를 다시 보자이 함수의 경우엔 강력한 보장을 거의 제공하는 것은 그다지 어렵지 않습니다.첫째로, PrettyMenu bgImage 데이터 멤버의 타입을 기본제공 포인터 타입인 Image*에서 자원관리 전담용 포인터(항목13)로 바꿉니다자원 누출을 막는 대책으로 본다면 이렇게 가는 게 딱 맞습니다.사용자에게 강력한 예외 안전성 보장을 제공할 수 있게 만든 것뿐인데 객체(스마트 포인터 등)를 써서 자원을 관리하는 것이 좋은 설계의 첫걸음(항목13)아래 코드에서 자원관리용 객체로 tr1::shared_ptr을 쓸 겁니다. ato_ptr도 있긴 하지만복사될 때의 동작이 더 직관적이라 사용하기가 더 좋거든요.
둘째로, changeBackground 함수 내의 문장을 재배치해서 배경그림이 진짜로 바뀌기 전에는 imageChanges를 증가시키지 않도록 만듭니다어떤 동작이 일어났는지를 나타내는 객체를 프로그램 내에서 쓰는 경우에는해당 동작이 실제로 일어날 때까지 그 객체의 상태를 바꾸지 않는 편이 좋다고 하지요.

class PrettyMenu {
public:
           ...
           std::tr1::shared_ptr<Image> bgImage;
           ...
};
void Pretty::changeBackground(std::istream& imgSrc)
{
           Lock m1(&mutex);
           bgImage.reset(new Image(imgSrc)); // bgImage의 내부 포인터를 “new Image” 표현식의 실행 결과로 바꿔치기합니다.
          
           ++imageChanges;
}

이제는 이전의 배경그림(Image 객체)을 프로그래머가 직접 삭제할 필요가 없게 되었습니다지금은 배경그림이 스마트 포인터의 손에서 관리되고 있기 때문입니다.게다가새로운 배경그림이 제대로 만들어졌을 때만 이전 배경그림의 삭제 작업이 이루어지도록 바뀐 점도 눈에 들어옵니다.다시 말해이제는 tr1::shared_ptr::reset 함수가 호출되려면 이 함수의 매개변수(“new Image(imgScr)”의 결과)가 제대로 생성되어야 한다는 것입니다.
delete 
연산자는 reset 함수 안에 쏙 들어 있기 때문에, reset이 불리지 않는 한 delete도 쓰일 일이 없을 것입니다
.객체(tr1::shared_ptr)를 써서 자원(동적 할당된 Image 객체)을 관리하게 하니까 changeBackground 함수의 길이까지 줄어들었습니다.

매개변수 imgScr, Image 클래스의 생성자가 실행되다가 예외를 일으킬 때그 시점에 입력 스트림의 읽기 표시자가 이동한 채로 남아 있을 가능성이 충분히 있을 테고이 표시자의 이동이 전체 프로그램의 나머지에 영향을 미칠 수 있는 어떤 변화로 작용할 수도 있을 것입니다.따라서 엄밀히 말하면 changeBackground가 제공하는 예외 안전성 보장은 기본적인 보장입니다.

예외 안전성 보장을 제공하는 함수로 거듭나게 만드는 일반적인 설계 전략을 하나 알아보도록 하죠이 전략은 복사 후 – 맞바꾸기(copy-and-swap)’라는 이름으로 알려져 있는데원리적으로 무척 간단합니다어떤 객체를 수정하고 싶으면 그 객체의 사본을 하나 만들어 놓고 그 사본을 수정하는 것입니다이렇게 하면 수정 동작 중에 실행되는 연산에서 예외가 던져지더라도 원본 객체는 바뀌지 않은 채로 남는 거죠필요한 동작이 전부 성공적으로 완료되고 나면 수정된 객체를 원본 객체와 맞바꾸는데이 작업을 예외를 던지지 않는’ 연산 내부에서 수행합니다.

이 전략은 대개 진짜’ 객체의 모든 데이터를 별도의 구현(implementation) 객체에 넣어두고그 구현 객체를 가리키는 포인터를 진짜 객체가 물고 있게 하는 식으로 구현합니다.
‘pimp 
관용구라고들 부르는 이 구현 방법은 항목31에서 
이 방법을 PrettyMenu에 적용한 코드가 다음이다.

struct PMImpl {                            // PMImpl = “PrettyMenuImpl”
           std::tr1::shared_ptr<Image> bgImage; // PMImpl struct로 선언된 데에는 이유가 있습니다아래에서 확인하세요.
           int imageChanges;
};

class PrettyMenu {
           …
private:
           Mutex mutex;
           std::tr1::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& imgScr)
{
           using std::swap; // 항목25.
           Lock m1(&mutex);
           std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(&pImpl)); // 객체의 데이터 부분을 복사합니다.
           pNew->bgImage.reset(new Image(imgScr)); // 사본을 수정합니다.
           ++pNeww->imageChanges;
           Swap(pImpl, pNew); // 새 데이터로 바꿔 넣어 진짜로 배경그림을 바꿉니다.
}

복사--맞바꾸기’ 전략은 객체의 상태를 전부 바꾸거나 혹은 안 바꾸거나(all-or-nothing)’ 방식으로 유지하려는 경우에 아주 그만입니다그러나 함수 전체가 강력한 예외 안전성을 갖도록 보장하지는 않는다는 것이 일반적인 정설입니다.
왜 그럴까요? changeBackground 함수의 전체 흐름을 추상화해 놓은 someFunc()를 한번 살펴봅시다. ‘복사--맞바꾸기’ 수법을 쓰되, f1  f2라는 다른 함수의 호출문이 들어 있는 형태로 말이죠다음과 같은 형태로 나올 겁니다.
void somdFunc()
{
… // 이 함수의 현재 상태에 대해 사본을 만들어 놓습니다.
f1();
f2();
… // 변경된 상태를 바꾸어 넣습니다.
}

f1 혹은 f2 에서 보장하는 예외 안전성이 강력하지 못하면위의 구조로는 someFunc 함수 역시 강력한 예외 안전성을 보장하기 힘들어집니다예를 들어 f1이 기본적인 보장만 제공한다고 가정하면, someFunc 함수에서 강력한 보장을 제공하게 만들려면 (1) f1을 호출하기 전에 프로그램 전체의 상태를 결정하고 (2) f1에서 발생하는 모든 예외를 잡아낸 후에 (3) 원래의 상태로 되돌리는 코드를 작성해야 합니다.

f1  f2 모두가 강력한 예외 안전성을 보장한다고 해도 사실 별로 나아지는 것은 없습니다예를 들어 어차피 f1이 끝까지 실행되고 나면프로그램 상태는 f1에 의해 어떻게든 변해 있을 것이고그 다음에 f2가 실행되다가 예외를 던지면 그 프로그램의 상태는 someFunc가 호출될 때의 상태와 아예 달라져 있을 것이니까요. f2에서 아무것도 바꾸지 않았더라도 말입니다.

여기서 불거지는 문제가 바로 함수의 부수효과(side effect)입니다.자기 자신에만 국한된 것들의 상태를 바꾸며 동작하는 함수의 경우(예를 들어 someFunc는 이 함수의 내부에서만 사용하는 객체의 상태에만 영향을 주고 있죠)에는 강력한 보장을 제공하기가 비교적 수월합니다그렇지만 비지역 데이터에 대해 부수효과를 주는 함수는 이렇게 하기가 무척 까다롭습니다.

강력한 예외 안전성 보장을 제공하게 하고 싶어서 아무리 열을 내더라도 이런 문제 때문에 발목을 잡힐 수 있다는 사실을 알고 계셨으면 좋겠습니다효율 문제도 무시할 수 없습니다. ‘복사--맞바꾸기’ 방법의 요체는 객체의 데이터에 대해 사본을 만들어 놓고 그 사본을 변경한 후에사본과 원본의 바꿔치기 작업을 예외를 던지지 않는 함수 내부에서 하자는 아이디어 입니다때문에 수정하고 싶은 객체를 복사해 둘 공간과 복사에 걸리는 시간을 감수해야 하겠지요이런 부분에 여유가 없거나 왠지 꺼림칙한 분이 분명히 있을 것입니다어쨌든 예외 안전성 보장 중에는 강력한 보장이 가장 좋습니다실용이 확보되는 경우라면 반드시 제공하는 게 맞고요. 그러나 언제나 실용적인 것은 아니랍니다.

l  예외 안전성을 갖춘 함수는 실행 중 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않습니다이런 함수들이 제공할 수 있는 예외 안전성 보장은 기본적보장강력한보장예외금지보장이 있습니다.
l  강력한 예외 안전성 보장은 복사--맞바꾸기’ 방법을 써서 구현할 수 있지만모든 함수에 대해 강력한 보장이 실용적인 것은 아닙니다.
l  어떤 함수가 제공하는 예외 안전성 보장의 강도는그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않습니다.

[Effective C++] 항목 15 : 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자

(15)        자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자

std::tr1::shared_ptr<Investment> pInv(createInvestment()); (13)예제

이때 어떤 Investment 객체를 사용하는 함수로서여러분이 사용하려고 하는 것이 다음과 같다고 가정.
Int dayHeld(const Investment *pi) // 투자금이 유입된 이후로 경과한 날수

그리고 이렇게 호출하고 싶을 텐데요.
Int days = daysHeld(pInv); // 에러!
애석하게도 이 코드는 컴파일이 안됩니다.
daysHeld 
 Investment* 를 원하는데
여러분은 tr1::shared_ptr<Investment> 을 넘기고 있기 때문입니다.

그래서, RAII 클래스(지금의 tr1::shared_ptr)의 객체를그 객체가 감싸고 있는 실제 자원(그러니까 Investment*)으로 변환할 방법이 필요해집니다.
이런 목적에 일반적인 방법을 쓴다면 2가지가 있는데하나는 명시적 변환(explicit conversion)이고다른 하나는 암시적 변환(implicit conversion)입니다.

tr1::shared_ptr  auto_ptr 은 명시적 변환을 수행하는 get이라는 멤버 함수를 제공합니다.이 함수를 사용하면각 타입으로 만든 스마트 포인터 객체에 들어 있는 실제 포인터(의 사본)를 얻어낼 수 있습니다.

int days = daysHeld(pInv.get()); // 이제 문제없습니다. pInv에 들어 있는 실제 포인터를 daysHeld에 넘기니까요.

제대로 만들어진 스마트 포인터 클래스라면 거의 모두가 그렇듯,
tr1::shared_ptr
 auto_ptr은 포인터 역참조 연산자(operator->  operator*)도 오버로딩하고 있습니다따라서 자신이 관리하는 실제 포인터에 대한 암시적 변환도 쉽게 할 수 있습니다.

class Investment { // 여러 형태의 투자를 모델링한 클래스 계통의 최상위 클래스
public:
           bool isTaxFree() const;
           ...
};

Investment* createInvestment(); // 팩토리 함수.

std::tr1::shared_ptr<Investment> pi1(createInvestment()); // tr1::shared_ptr이 자원관리를 맡도록 합니다.
bool taxable1 = !(pi1->isTaxFree()); // operator->를 써서 자원에 접근합니다.
...
std::tr1::shared_ptr<Investment> pi2(pInv1); // auto_ptr이 자원관리를 맡도록 합니다.
bool taxable2 = !((*pi2).isTaxFree()); // operator*를 써서 자원에 접근합니다.
...

RAII 객체 안에 들어 있는 실제 자원을 얻어낼 필요가 종종 생기기 때문에,
RAII 
클래스 설계자 중에는 암시적 변환함수를 제공하여 자원 접근을 매끄럽게 할 수 있도록 하기도 한다.

하부 수준 C API로 직접 조작이 가능한 폰트를 RAII 클래스로 둘러싸서 쓰는 경우.
class Font { // RAII 클래스
public:
           explicit Font(FontHandle fh) : f(fh) {} // 자원을 획득합니다여기서 값에 의한 전달이 수행되는 것에 주의자원해제를 C API로 하기 때문.
           ~Font() { releaseFont(f); }
          
private:
           FontHandle f; // 실제 폰트 자원
};

Font 클래스에서는 명시적 변환 함수로 get을 제공할 수 있다.
class Font {
public:
           ...        
           FontHandle get() const { return f; } // 명시적 변환 함수
           ...
};
이렇게 해두면 쓸 수 있긴 한데사용자는 하부 수준 API를 쓰고 싶을 때마다 get을 호출해야 할 것이다.
void changeFontSize(FontHandle f, int newSize); // 폰트 API의 일부

Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize); // Font에서 FontHandle로 명시적으로 바꾼 후에 넘깁니다.

변환할 때마다 함수를 호출하는 것이 번거로울 수 있다.대신 FontHandle로의 암시적 변환 함수를 Font에서 제공하도록 하면 된다.
class Font {
public:
           ...        
           operator FontHandle() const { return f; } // 암시적 변환 함수
           ...
};

암시적 변환 함수로 C API를 사용하기가 훨씬 쉬워지고 자연스러워진다.
void changeFontSize(FontHandle f, int newSize); // 폰트 API의 일부

Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize); // Font에서 FontHandle로 암시적 변환을 수행합니다.

그러나암시적 변환이 들어가면 실수를 저지를 여지가 많아진다.
Font
를 쓰려고 한 부분에서 원하지도 않았는데, FontHandle로 바뀔 수도 있다는 거죠.
Font f1(getFont());
...
FontHandle f2 = f1; // 원래 의도는 Font 객체를 복사하는 것이었는데엉뚱하게도 f1 FontHandle로 바뀌고 나서 복사되어 버렸습니다.
이렇게 되면 Font 객체인 f1이 관리하고 있는 폰트(FontHandle)가 f2를 통해서도 직접 사용할 수 있는 상태가 됩니다.하나의 자원이 양다리를 걸치고 있는이와 같은 상황은 좋지 않습니다.
f1
이 소멸될 시점에 폰트가 해제될 텐데그럼 f2는 해제된 폰트에 매달려 있는 모양이 된다.

RAII 클래스를 실제 자원으로 바꾸는 방법으로서명시적 변환을 제공할 것인지(get 멤버 함수 등아니면 
암시적 변환을 허용할 것인지에 대한 결정은 RAII 클래스만의 특정한 용도와 사용환경에 따라 달라집니다어쨌든 가장 잘 설계한 클래스라면 (18)에 따라 맞게 쓰기에는 쉽게틀리게 쓰기에는 어렵게” 만들어져야 할 것입니다.늘 그런 것은 아니지만암시적 변환보다는 get 등의 명시적 변환함수를 제공하는 쪽이 나을 때가 많습니다원하지 않은 타입 변환이 일어날 여지를 줄여주는 것은 확실하니까요.하지만 암시적 타입 변환에서 생기는 사용 시의 자연스러움이 빛을 발하는 경우도 있다는 점도 알아두세요.

RAII 클래스에서 자원 접근 함수를 열어 주는 설계가혹시 캡슐화에 위배되는 것이 아닌지 생각할 수도 있다그렇긴 하지만 틀린 설계도 아니다.
RAII 
클래스는 애초부터 데이터 은닉이 목적이 아니다
.원하는 동작(자원 해제)이 실수 없이 이루어지도록 하면 OK인 것.굳이 원한다면 자원 해제라는 기본 기능 위에 캡슐화 기능을 얹을 수는 있겠지만꼭 필요한 것은 아니다.시중의 RAII 클래스 중에는 이미 자원의 엄격한 캡슐화와 느슨한 캡슐화를 동시에 지원하는 것들 것 꽤 있다. tr1::shared_ptr이 대표적 예인데이 클래스는 참조 카운팅 매커니즘에 필요한 장치들은 모두 캡슐화하고 있지만그와 동시에 자신이 관리하는 포인터를 쉽게 접근할 수 있는 통로도 여전히 제공하고 있다.꼼꼼히 제대로 설계된 클래스가 그렇듯사용자가 볼 필요가 없는 데이터는 가리지만고객차원에서 꼭 접근해야 하는 데이터는 열어 주는 것입니다.

l  실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문에, RAII 클래스를 만들 때는그 클래스가 관리하는 자원을 얻을 수 있는 방법을 열어 주어야 합니다.
l  자원 접근은 명시적 변환 혹은 암시적 변환을 통해 가능합니다안전성만 따지면 명시적 변환이 대체적으로 더 낫지만고객 편의성을 놓고 보면 암시적 변환이 괜찮습니다.

[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 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

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