본문 바로가기

개발 Note/it 이야기

C++를 제대로 알자!. Effective C++ , More Effective C++

반응형


스콧 마이어스 

참. 주옥같은 한줄 한줄이 들어있는 책이죠.
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에 해당합니다. 프로그램의 성능에 영향을 크게 미치는 것이 아닙니다.
단지 예외 발생이 자주 일어난다면, 그때는 예외를 사용할 것인지 심각하게 고려해봐야 합니다.




오 남용.