예외가 소멸자를 떠나지 못하도록 붙들어 놓자!


그냥 소멸자안에서 예외처리를 하면 안된다는 말인거 같다.

예제를 보며 문제점과 해결방안을 짚어보겠다.


class NPC{

public:

...

~NPC() { ... };

--> 예외를 던지는 소멸자라고 가정

};


void doSomething(){

vector<NPC> n;

...

}


vector 타입의 객체 n은 자신이 소멸될 때

자신이 가지고 있는 NPC객체를 소멸시킬 책임이 있다.

하지만 NPC를 소멸시키는 도중 예외가 발생되었다고 하자.

이 경우 프로그램은 미정의 동작을 보인다.

원인은 바로 예외를 내보내는 소멸자 때문!!

(NPC의 소멸자에서 예외를 내보내고 있다)




무슨 말인지 모르겠다. 다음 예를 보도록 하자.

class DBConnection {

public:

...

static DBConnection create();


void close();  

--> 연결을 닫는 함수. 연결이 실패하면 예외를 던진다고 가정.

--> 즉, 이번 항목 핵심인 예외를 던지는 함수

};


class DBConn{

private:

DBConnection db;

public:

...

~DBConn(){

db.close();

--> 소멸자에서 예외를 던지는 함수를 호출하고 있다.

}    

};


위의 예제는 데이터베이스의 연결을 관리하는

클래스 DBConnection과 그의 자원은 관리하는

클래스 DBConn이다.

여기서 주목할 점은 DBConnection의 멤버 함수

close()는 실패할 경우 예외를 던진다는 점이다.


{

DBConn dbc(DBConnection::create());

...

...

}


--> DBConnection 객체를 생성하고 DBConn으로

넘겨주어 관리를 맡기고 있다.

블록이 끝나면 DBConn 객체가 사라지고

사라지면서 자연스레 close()함수를 불러 연결을 닫게 된다.




하지만 close()는 실패할 경우 예외를 던진다!

때문에 close()를 호출하는 DBConn의 소멸자는

이 예외를 전파할 것이다. --> 문제 발생



해결책

1. 예외가 발생하면 프로그램을 끝내버린다.

~DBConn::~DBConn(){

try{ db.close() }

catch( ... ) {

...

abort();

}

}


-->  close()에서 예외를 던졌다.

                                                                                                          걍 프로그램을 끝내버린다.

                                                                                          미정의 동작으로 가기보다는 그냥 끝내버리겠다는 방법




2. 예외가 발생하면 삼켜버리겠다.

~DBConn::~DBConn(){

try{ db.close() }

catch( ... ) {

...

}

}


--> 그냥 무시하고 진행하겠다는 것.

      무엇이 잘못되었는지 알 수 없다.

      예외를 무시하더라도 그 다음 동작이

      제대로 이루어 진다는 보장이 있을 때 사용.




--> 두가지 모두 좋은 해결책은 아니다.

      사용자가 직접 해결할 수 잇도록 기회를 주도록 하자!


class DBConn{

private:

DBConnection db;

bool closed;

public:

...

void close(){

db.close();

closed = true;

}


~DBConn(){

if(!close)

try{

db.close();

}

catch( ... ){

 ...

}

}

};

--> 사용자가 연결을 직접 닫을 수 있게 close()함수를 제공

--> 즉, 사용자가 함수를 직접 불러 연결을 닫음

--> 따라서 발생하는 예외 또한 사용자가 처리해야함.




--> DBConn 객체가 소멸될 때, 사용자가 이미 close()함수를 불러

연결을 닫은 후라면, 소멸자에서 예외가 발생할 경우가 없어지게 된다.

    --> 연결이 열려있다면 소멸자 내의 db.close() 함수를 호출해서 닫아 준다.

--> 닫다가 실패하면 위의 두가지 방법 중 하나

      예외를 먹던지, 종료를 하던지 한다.




같은 말을 계속 반복하는거 같지만

쉽게 말해서 DBConn 클래스에 close()함수로

DBConnection의 연결을 직접 닫겠다는 것이다.

DBConn의 close()를 호출하기 되면 closed = true가 될 것이고

DBConn의 소멸자에서 예외를 던지지 않게되는 것이다.

--> 쉽게 말해 사용자가 직접 닫아라.



여기서 핵심!!

--> 예외는 소멸자가 아닌 다른 함수에서 비롯되어야 한다.

예외를 던지는 소멸자는 시한폭탄과도 같다.



POINT!!

1. 소멸자에서는 예외가 던져지면 안된다. 

   소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면

   삼키던지 종료하던지 해라.


2. 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 

   사용자가 직접 처리해야 할 경우, 해당 연산을 제공하는 함수는

   반드시 보통의 함수여야 한다. (소멸자 노노)






팩토리 함수


팩토리 함수, 팩토리 메소트, 팩토리 메소드 패턴이라 불리는데

객체를 생성하기 위한 인터페이스를 제공하는데, 어떤 클래스의

인스턴스를 만들지는 서브클래스에서 결정한다.

즉, 클래스의 인스턴스를 만드는 일은 서브클래스가 한다는 말이다.




팩토리 메서트 패턴은 추상클래스에서 객체를 만드는 인터페이스만 제공한다.

추상클래스는 어떤 객체가 생성될지 전혀 모른다.

어떤 객체가 생성될 지는 이 추상클래스를 상속받은

서브클래스에서 실제로 팩토리 함수를 구현함으로써 이뤄진다.


말로만 해서는 모르겠다. 예제를 보도록하자.


MonMgr이라는 클래스는 객체를 생성하기 위한 인터페이스만 제공한다.

그리고 객체의 포인터를 리턴하는 팩토리 함수를 멤버로 가지고 있으며

MonMgr의 파생클래스에서 이 함수를 구현함으로써 어떤 객체가 생성될지 결정된다.




예제가 많이 허접하다. 그래도 팩토리 함수가 무엇인지는 감이 오는 것 같다.

그렇다면 이 팩토리 함수를 어떤 경우에 써야 효율적인 것일까.



1. 구체적으로 어떤 객체를 생성해야할지 알 수 없는 경우

2. 하위 클래스가 객체를 생성하기를 원할 때

3. 하위 클래스들에게 개별 객체의 생성 책임을 분산시켜

객체의 종류별로 객체 생성과 관련된 부분을 국지화 시킬 때



음.. 어떤 것인지는 알았으나 어떤 경우에 써먹어야 하는지

아직은 모르겠다.. 코딩 경험이 쌓이다보면 자연스레 알게되겠지

'C++' 카테고리의 다른 글

[C++ STL] 연산자 오버로딩  (0) 2016.12.19
[C++] 전방 선언  (0) 2016.12.19
[C++] API란?  (0) 2016.12.15
[C++] 예외처리 // 열혈강의  (0) 2016.12.14
[C++] 템플릿 // 열혈강의  (0) 2016.12.12


다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자!


시간 기록을 유지하는 클래스


class TimeKeeper{

public:

TimeKeeper();

~TimeKeeper();

...

};


class AtimicClock : public TimeKeeper { ... };

class WaterClock : public TimeKeeper { ... };

class WristWatch : public TimeKeeper { ... };




시간정보에 접근하기 위한 팩토리함수를 선언

TimeKeeper* getTimeKeeper(){ return new 파생 클래스 };


팩토리 함수의 기존 규약을 그대로 따라간다면 이 함수에서

리턴되는 객체는 힙에 있게 된다. 따라서 삭제를 해줘야함.


TimeKeeper *ptk = getTimeKeeper();

...

...

delete ptk;


문제점


getTimeKeeper()에서 리턴되는 포인터가 파생 클래스 객체에 대한

포인터라는 점과 이 포인터가 가리키는 객체가 삭제될 때는

기본 클래스 포인터를 통해 삭제된 다는 점.


--> 기본 클래스의 소멸자가 비가상 소멸자이기 때문에

자기자신만 지우고 파생클래스는 지우지 않는다




C++의 규정에 의하면, 기본 클래스 포인터를 통해 파생 클래스 객체가

 삭제될 때, 그 기본 클래스에 비가상 소멸자가 들어 있으면

 프로그램 동작은 미정의 사항이라고 되어있다.


머리 아프다 맘 편히 정리하자

가상 함수를 하나라도 가진 기본 클래스라면 가상 소멸자를 가져야 한다!




하지만 기본 클래스로 의도하지 않은 클래스에 대해

 소멸자를 가상으로 선언하는 것은 좋지 않다.

가상 함수 테이블 포인터와 가상 함수 테이블에

 대한 얘기가 나오는데, 이 부분은 나중에 다시 심도있게 다루겠다.

 아무튼 기본 클래스가 아니라면 가상소멸자를 쓰지말자.


중간 정리

가상 소멸자를 선언하는 것은 그 클래스에 가상 함수가 하나라도 들어 있는 경우에만 한정하라.




비가상 소멸자의 예 --> 표준 string 클래스


class sss : public string {

...

};


sss* psss = new sss("Good Morning");

string *ps = psss;

delete ps;             --------> sss의 소멸자가 호출되지 않는다.


STL 컨테이너 타입도 비가상 소멸자이다.

따라서 STL을 상속받아서 나만의 클래스를 만드는 것은 금물!

(자바의 final이나 C#의 sealed 클래스는 파생방지 메커니즘이 따로 있다.)




순수 가상 소멸자

어떤 클래스가 추상 클래스였으면 하는데

 마땅히 순수 가상으로 만들 함수가 없을 때

순수 가상 소멸자를 지정해주는 것도 좋은 방법이다.

(추상 클래스 --> 기본 클래스 목적으로 만들어짐.)


주의!

순수 가상 소멸자는 정의를 꼭 두어야 한다.


class sss{


public:

sss();

virtual ~sss();

}

sss::~sss();




소멸자의 동작 순서

상속 계통 구조에서 가장 말단에 있는 파생 클래스의

소멸자가 먼저 호출 되고 점점 올라오면서 차례로 소멸자가 호출된다


컴파일러는 ~sss의 호출 코드를 만들기 위해 파생 클래스의

소멸자를 사용할 것이므로, 잊지 말고 이 함수의 본문을 준비해 두어야

하는 것이다. 이 부분을 잊는다면 링커 에러!




잊지말기

가상 소멸자를 선언해주는 규칙은 다형성을 가진 기본 클래스,

기본 클래스 인터페이스를 통해 파생 클래스 타입의 조작을 허용하도록

설계된 기본 클래스에서만 적용된다는 사실!




기본 클래스의 인터페이스를 통해 파생클래스의 객체 조작이 허용될 때만 가상 소멸자!!

앞서 말한 string과 stl은 다형성을 가지지 않음

그리고 전전 포스팅에서 uncopyable이라는 복사, 대입 금지 클래스를 만들었었다.

기본 클래스로는 쓰이고 있지만 다형성을 가지지 않는 클래스라 말할 수 있다.

이러한 경우도 가상소멸자 안됨~



POINT!!

다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 한다.

즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 닥치고 가상 소멸자.


기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는

가상 소멸자를 쓰면 안된다!!



+ Recent posts