스콧 마이어스
참. 주옥같은 한줄 한줄이 들어있는 책이죠.
GOF 디자인 패턴과 함께 손에 놓을 수가 없네요. 왠만한 소설책보다 재미있다는...ㅋ
2판은 후배가 본다고 해서, 서점에 들려서 3판을 주문해서 다시 보게 되었답니다. ㅎ
[Effective C++]
복사생성자와 대입연산자 처리 - 만약 내가 만들려는 class가 만약 복사가 필요없다면, 원천 차단 하라.!
- 항목 6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해버리자!
복사 생성자와 대입 연산자 private으로 선언하기!
복사 생성자와 대입 연산자 예기가 나오니 몇달전에 겪었던 한 사건이 생각나네요. ㅎ
DLL 모듈을 작성하면서 만든 class가 있었는데, 우연찮게 다른 프로젝트에서 해당 class 와 같이 들어있던 define이 필요해서 include를 한적이 있었습니다. 다른 class들은 다 문제가 없었는데 유독 한 class에서 link error가 발생하더군요.
__declspec(dllexport) 로 선언되어있는 것이 문제긴 했는데, 다른 class들은 다 문제가 없는데 왜?????? 이 class만 문제가 되는가로 고민을 하게 되었습니다.
그런데 원인은 알겠는데, 그렇다면 똑같이 선언된 다른 class들은 왜 문제가 없었는가? 하는것이었죠.
이걸로 한 2시간 고민하다가 해더파일의 class코드를 다 지우고, 문제가 발생하지 않은 녀석과 문제가 발생한 녀석 2개를 놓고, 한줄 한줄 copy하면서 비교해 봤습니다.
결국 두 클래스간의 차이는 복사연산자와 대입연산자가 금지되어있는가 아닌가 의 차이더군요.
대입연산자 (operator =)를 금지 시켰더니 문제가 해결되더군요.
변수 초기화
C와 C++의 차이.
우선 C는 기본적으로 변수 초기화가 안되죠. C++은 객체이기 때문에 객체를 생성하는 시점에 초기화가 기본적으로 되죠.(constructor 불리니까 말이죠).
C의 경우 물론 다른 정책을 가지고 있습니다. C++ 예기에 주로 관심이 있으니 넘어가도록 하겠습니다.
항목 4: 객체를 사용하기 전에 반드시 그 객체를 초기화 하자.
를 살펴보면 이런 대목이 있습니다.
"C++의 객체 초기화가 중구난방인것은 아니다. 언제 초기화가 보장되며 언제 그렇지 않은지에 대한 규칙이 명확히 준비되어있다. 그런데 복잡하다."
뭐 대충 이런 구문인데,과연 무엇이 복잡한가?
정적객체(static object) 중에서 비지역적 정적 객체와 지역 정적 객체가 있는데, 함수내의 static object만 지역 정적 객체이고 나머지는 비 지역적 정적객체입니다.
비 지역적 정적 객체는 compiler가 생성시점을 결정하기 때문에 생성 순서나 초기화 순서를 개발자가 결정할수 없게 됩니다.
때문에 비지역적 정적객체를 지역 정적객체로 바꿔서 사용하는 것이 일반적입니다.
CWindowManager * CWindowManager::GetInstance()
{
static CWindowManager windowManager;
return &windowManager;
}
또는
CWindowManager * CWinMgr(void)
{
static CWindowManager windowManager;
return &windowManager;
}
이런식이죠. 말은 어렵지만 코드는 단순합니다. ㅎ
코딩할때는 이렇게 사용하겠죠.
CWindowManager * pWinMgr = CWinMgr();
또는
CWindowManager * pWinMgr = CWindowManager::GetInstance();
pWinMgr->GetWindow(x,y);
컴파일러의 종류나 구현에 따라 다르게 동작하는 부분
- 항목17: new로 생성한 객체를 스마트 포인터로 저장하는 코드는 별도의 한 문장으로 하자.
책에 있는 예제로 가보면,
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw,int priority);
:
processWidget(std::tr1::shared_ptr<Widget>(new Widget) , priority());
의 예제 코드를 보면, resource leak이 발생할 가능성이 있답니다.
컴파일러가 processWidget 호출 코드를 만들기 전에 우선 이 함수의 매개변수로 넘겨지는 인자를 확합니다.
이때 첫번째 인자인 std::tr1::shared_ptr<Widget>(new Widget)는 2부분으로 나뉩니다.
- new Widget
- tr1::shared_ptr -> 생성자 호출부분
때문에 processWidget을 호출하기 전에, 컴파일러는
priority()를 호출하는 부분,
new Widget 을 실행하는 부분.
tr1::shared_ptr 생성자를 호출하는 부분.
위와 같이 3부분으로 나뉜다고 한다면, 컴파일러에 따라, priority() -> new Widget -> tr1::shared_ptr 생성자 호출의 순서가 될 수도 있고, new Widget -> priority() -> tr1::shared_ptr 생성자 호출 이 순서가 될 수도있습니다.
두번째 경우 priority() 에서 exception이 발생한다면, new Widget으로 생성한 포인터는 유실될 가능성이 있습니다.
자원 유실을 막기 위해 shared pointer 를 사용했는데, 예측할 수 없는 위치에서 자원이 유실된 케이스겠죠.
이럴 바에야
std::tr1::shared_ptr<Widget>pw(new Widget);
processWidget(pw,priority());
를 따로 써서 고민꺼리를 미리 예방하는게 좋겠다.!!! 는 것...
강력한 보장: 함수가 호출되고 내부에서 error 처리 루틴에 의해 fail을 return할때는 함수가 호출된적이 없었던 것 처럼 깔끔하게 프로그램 상태가 돌아가게 한다.는...
- 항목 29:예외 안정성이 확보되는 그날 위해 싸우고 또 싸우자!
Hiding: ????
-항목 33:상속된 이름을 숨기는 일은 피하자
음.. 아마 대부분의 경우 의도적으로 상속된 멤버의 이름을 숨기는 일은 별로 없을것이다.
우연찮게, 또는 실수에 의해서 HIDING되는 경우가 많다.
- 이런 경우 정말 c++에 대해서 잘 알지 못하면, 문제의 원인을 알아내기 힘들다. 또 잘알고 있더라도 찾아내기 힘들다.
다행히도 이럴때 compiler가 warning을 내주긴 한다.
다음 예제를 보자!(Effective C++에 나온 예를 노가다 타이핑 했습니다. ㅡㅡ;;)
class Base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
virtual void mf3();
virtual void mf3(double);
...
};
class Derived: public Base
{
public:
virtual void mf1();
void mf3();
void mf4();
};
void test_main(void)
{
Derived d;
int x;
...
d.mf1(); //좋습니다. Derived::mf1을 호출합니다.
d.mf1(x);//에러. Derived::mf1이 Base::mf1을 가립니다.
d.mf2();//좋습니다. Derived::mf2을 호출합니다.
d.mf3();//좋습니다. Derived::mf3을 호출합니다.
d.mf3(x);//에러. Derived::mf3이 Base::mf3을 가립니다.
}
어 ! 왜 에러지? 이런 의문을 품게 될것입니다. "상속받은 서브클레스가 슈퍼클레스의 멤버를 호출하는데 왜 에러지?" 라고 말이죠.!!
하이딩에 대해서 모르고 있기 때문입니다.
C++ 의 네이밍 규칙중에 하나인, 기본 클래스로부터 오버라이드 버전을 상속시키는 경우는 막겠다.
즉, 슈퍼클레스에 정의된 이름과 동일한 이름을 서브클레스에서 정의해서 사용하게 되면, 서브클래스 내에서는 슈퍼클레스의 이름을 모두 가리겠다(hiding)는 의미입니다.
"warning: Deriver::mf1() hides virtual Base::mf1()" 이런 워닝을 보이면서 말이죠.
[More Effective C++]
예외(Exception)
"예외 처리는 프로그램을 여러가지 면에서 흔들어 놓습니다." 그 증세는 심각하고 급진적이며 불편하기까지 합니다.
초기화 되지 않은 포인터를 사용하는 것이 위험해지고, 리소스 누수가 발생할 가능성의 가짓수도 늘어납니다.
우리가 원하는 대로 작동하는 생성자와 소멸자를 만들기가 보다 더 힘들어집니다.
예외처리 기능이 들어간 실행 파일과 라이브러리는 덩치도 늘어나고 속도도 느려집니다.
또 어떻게 해야 제대로 사용하는지 모릅니다.(예외가 발생했을때 어떻게 해야 적절하고 안정적으로 동작하게 하는 벙법에 대해서 의견일치가 아직도 이뤄지지 않았다는 것입니다.)
여기 내용을 찾아보시기 바랍니다. -
EXCEPTIONHANDLING:
A FALSE SENSE OF SECURITY
byTom Cargill
setjump 와 longjump 는 exception 처리하는 루틴과 비슷하다. 차이라면, 스택을 거슬러 올라가면서 스택 내의 c++ 객체의 소멸자를 호출하지 않는 다는 것이다.
항목 9: 리소스 누수를 피하는 방법의 정공은 소멸자이다.
void processAdoptions(istream & dataSource)
{
while(dataSource)
{
ALA* pa = readALA(dataSource);
try{
pa->processAdoption(); // 예외를 던질 녀석
}
catch(...)
{
delete pa;
throw;
}
delete pa;
}
}
위와 같이 pa->processAdoption()에서 예외를 던질것을 예상하고 코드는 복잡하게 처리되기 마련이다.
이를 스마트 포인터를 써서 구현하게 되면 훨씬 간결한 코드가 나오게 된다.
void processAdoptions(istream &dataSource)
{
while(dataSource)
{
auto_ptr<ALA> pa( readALA(dataSource) );
pa->processAdoption(); // 예외를 던질 녀석
}
}
위 처럼 pa->processAdoption()가 예외를 던지더라도 while 구문을 빠져 나갈때 auto_ptr<ALA>의 소멸자가 불리면서 내부에서 ALA의 소멸자를 호출하기 때문에, 훨씬 간결해지고, 리소스 관리가 편해지게 됩니다.
항목 10: 생성자에서는 리소스 누수가 일어나지 않게 하자
C++은 생성과정이 완료된 객체에 대해서만 안전하게 소멸 시킵니다. 만약 생성자 내부에서 exception이 발생하게 되면, 해당 객체의 소멸자는 호출되지 않습니다.
항목 11: 소멸자에서는 예외가 탈출하지 못하게 하자.
예외처리가 진행되고 있는 동안 다른 예외 때문에 소멸자를 떠나게 되면, C++은 terminate란 함수를 호출하게 되어있습니다.
이 함수는 프로그램의 실행을 끝장내기 때문에, 것도 아주 칼같이 끝내 버리기 때문에,지역 객체조차도 소멸되지 않습니다.
Session::~Session()
{
try{
logDestruction(this); // 예외를 던지는 녀석
}
catch(...){ } //expction으로 소멸자를 빠져 나가는 것을 잡고있는 녀석
}
위의 try catch구문만으로도 아주 훌륭한 소멸자 예외처리가 되는 것입니다.
항목 15: 예외처리에 드는 비용에 대해 정확히 파악하자
통상적인 함수 복귀(return)과 비교할때 , 예외 발생(throw)에 의한 함수 복귀 속도는 10의 세 제곱배(1000배) 만큼 느리다고 합니다.
예외 비용을 최소화 하려면, 가능하다면 예외 기능을 지원하지 않도록 컴파일하십시오.
try 블록과 예외 지정은 꼭 필요한 부분에만 사용합니다. 예외를 발생시키는 일도 진짜 예외적인 상황이라고 판단될 때에만 해야 합니다.
80-20 법칙 : 예외 자체는 사실 프로그램의 동작상 20에 해당합니다. 프로그램의 성능에 영향을 크게 미치는 것이 아닙니다.
단지 예외 발생이 자주 일어난다면, 그때는 예외를 사용할 것인지 심각하게 고려해봐야 합니다.
오 남용.