우리가 스마트포인터를 이용하여 객체를 관리할때

 

즉!

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

위와 같이 스마트 포이터를 선언하고, Investment 클래스를 사용하는 함수를 만들려고 할 때, 

 

int daysHeld(const Investment *pi);

이렇게 선언한 뒤

 

int days = daysHeld(pInv);

 

이렇게 사용하는 것이 일반적일 것이다. 그러나 이 구문은 에러가 난다.

왜냐하면 daysHeld의 전달인자는 Investment의 포인터형이지만, pInv는 std::tr1::shared_ptr<Investment> 타입의 객체이기 때문에 타입이 맞지 않아 에러가 발생하는 것이다. 

 

그래서,

스마트 포인터가 가리키는 실제 자원으로 변환해야 할 필요가 있는데, 여기서 방법은 명시적 변환과 암시적 변환이 있다.

 

첫째, 명시적 변환 

std::auto_ptr와 std::tr1::shared_ptr에는 실제 포인터를 명시적으로 얻을 수 있는 get()멤버함수를 제공한다. 

 

둘째, 암시적 변환

std::auto_ptr와 std::tr1::shared_ptr에는 자신이 관리하는 실제 포인터에 대한 암시적 변환도 쉽게 얻을 수 있게 operator-> 와 operator* 을 정의해 두었습니다.

( ex. 사용예 pi->isEmpty(), *(pi).isEmpty() 드러나 있지 않지만 변환이 실제 포인터로 변환이 이루어진 상태에서 자원에 접근 하고 있다. ) 

 

또한, 암시적 변환 함수를 이용해서 스마트 포인터 변수명만으로도 실제 포인터를 얻어낼수 있습니다.

operator 변환될 자료형 () const; ( ex. operator FontHandle() const { return f; } )

위의 변환을 사용하게 되면, 암시적으로 쉽게 내부 포인터를 얻어 올수 있게 되지만, 원하지 않는 변환이 일어 날 수도 있기때문에 조심해야한다.

 

1
2
3
4
Font f1(getFont());
...
 
FontHandle f2 = f1; /**
                     * 원래의도는 Font 객체를 복사하는 것이었는데
                     * 엉뚱하게도 f1이 FontHandle로 바뀌고 나서
                     * 복사되어 버림
                     */
 

 

꼼꼼히 제대로 설계된 클래스가 그렇듯, 사용자가 볼 필요가 없는 데이터는 가리지만 고객 차원에서 꼭 접근해야 하는 데이터는 열어 주는 것입니다.

 

 

이것 만은 잊지 말자!

◆ 실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문에, RAII 클래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법을 열어 주어야 합니다. 

◆ 자원 접근은 명시적 변환 혹은 암시적 변환을 통해 가능합니다. 안전성만 따지면 명시적 변환이 대체적으로 더 낫지만, 고객 편의성을 놓고 보면 암시적 변환이 괜찮습니다.


자원은 모두 Heap에서 생성되는 것은 아니기 때문에 동적 할당 객체가 아닌경우에는 스마트 포인터가 적절하지 않습니다.

 

이와 같은경우 사용자가 직접 자원관리 클래스를 만들어줘야 합니다.

 

다음과 같이 RAII 법칙을 따라 클래스를 구성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Lock 
 private
     Mutex* mutexPtr; 
 
 public
     explicit Lock(Mutex *pm) : mutexPtr(pm) 
     { 
         lock(mutexPtr); 
     } 
  
     ~Lock() 
     { 
         unlock(mutexPtr); 
     } 
};
 
void main()
{
    Mutex m;
    
    { //임계 영역을 정하기 위해 블록을 만듭니다.
        Lock m1(&m);
        
    } // 블록의 끝입니다. 뮤텍스에 걸렸던 잠금이
      // 자동으로 풀립니다.
}

 

이러한 RAII클래스를 구현했지만, 문제는 RAII 객체가 복사될때는 어떤 동작을 이루어져야 하는지가 참 힘들게 합니다. 실제로 RAII 클래스는 복사되도록 놔두는 것 자체가 말이 안되는 경우가 꽤 많습니다.

 

자 그럼 복사에 대한 문제해결을 하기위한 방법을 알아봅시다.

 

첫째, 복사를 금지합니다.  

복사하면 안되는 RAII클래스에 대해서는 반드시 복사가 되지 않도록 막아야 합니다. 사본이 필요없는경우 입니다. 위의 예제도 이 부류에 속합니다.( Uncopyable 클래스를 사용 )

 

둘째, 관리하고 있는 자원에 대해 참조 카운팅을 수행합니다.  

std::shared_ptr을 이용하여 참조하는 객체의 갯수가 0이 될때 그지정한 객체를 삭제시킵니다.

하지만, 위의 클래스의 예를 들자면 우리는 Mutex는 삭제가 아니고 ulock만을 원합니다.

이럴땐 std::shared_ptr의 생성자중에 삭제자를 지정할 수있는 생성자를 사용합니다.

 

삭제자란? tr1::shared_ptr이 유지하는 참조 카운트가 0이 되었을 때 호출되는 함수 혹은 함수 객체를 일컷습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
class Lock 
 private
     std::tr1::shared_ptr<Mutex> mutexPtr;
 
 public// unlock 함수포인터를 생성자 인자로 넘겨줌 (삭제자 지정)
     explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) 
     { 
         lock(mutexPtr); 
     }
    //소멸자가 사라짐
};

 

셋째, 관리하고 있는 자원을 진짜로 복사합니다.( deep copy )  

 자원의 깊은 복사를 수행하는 것입니다.( ex. std::string 타입의 복사 방식 ) 

 

넷째, 관리하고 있는 자원의 소유권을 옮깁니다. 

std::auto_ptr 처럼 소유권은 단 한개만 가지고 싶을경우

 

이것 만은 잊지 말자!

◆ RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정됩니다.

◆ RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해 주는 선으로 마무리 하는 것입니다. 하지만 이 외의 방법들도 가능하니 참고해 둡시다. 


 

아래의 예제를 봅시다.

 

1
2
3
4
5
6
void function()
{
    Inverstment * pInv = createInverstment(); //팩토리함수
    ...
    delete pInv; //객체 삭제
}

 

 

createInvestment() 함수를 통해 Investment 클래스의 포인터를 가져와서, pInv의 어떠한 동작을 수행한뒤, pInv의 메모리를 해제 합니다. 

 

정상적인 수행을 하게되면, 당연히도 delete pInv; 까지 내려가서 수행후 함수호출을 빠져나오는것을 생각하지만, 여기에선 return 문을 만나 바로 함수호출을 빠져 나간다든지, 많은 예외가 있습니다. 즉 ! 메모리 누수가 발생하게 됩니다.

 

우리는 createInvestment() 함수로 얻어낸 자원이 항상 해제되도록 만들어야 합니다. 그러기위해서는 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며, 그 소멸자는 실행 제어가 function()함수를 떠날 때 호출되도록 만드는 것입니다.

 

표준라이브러리를 보면 auto_ptr이 있는데 위에있는 용도에 쓰라고 마련된 클래스입니다.

 

std::auto_ptr< 생성할 클래스 > 변수명( 동적할당된 포인터 )

이렇게 자원을 획득(동적할당)한후 바로 자원 관리 객체에게 넘겨주는데, 자원 획득 하자마자 초기화 하는 이러한 방법을 RAII(Resource Acquisition is Initialization) 이라고 합니다.

 

std::auto_ptr 은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로 delete를 수행합니다.

그렇기 때문에 어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 절대로 안됩니다. 만약에 이런사태가 벌어지면 결국 자원이 두번 삭제 될테니 큰 문제가 됩니다.

 

위와같은 문제 때문에 std::auto_ptr은 객체를 복사하면, 원본 객체는 NULL로 만듭니다. 복사하는 객체만이 그 자원의 유일한 소유권을 갖는다고 가정합니다.

 

1
2
3
4
5
6
7
8
9
void function(){
    std::auto_ptr<A> pB1(new A());
 
 //복사되는 순간 pB1은 NULL, pB2가 A객체를 가르킴
    std::auto_ptr<A> pB2(pB1);
 
 // 역시 대입되는 순간 pB2는 NULL, pB1이 A객체를 가르킴
  pB1 = pB2;
}

 

 

여러번 할당해제되는 문제를 막을 수 있지만, 정상적인 복사 동작을 요구하는 STL컨테이너에서는 auto_ptr 객체를 원소로서 허용하지 않습니다.

 

그래서 그 대안으로 참조 카운팅 방식 스마트 포인터 (reference-counting smart pointer: RCSP)가 있다. RCSP는 특정한 어떤 자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터 입니다.

 

1
2
3
4
5
6
7
void f(){
    A * pA = create(); //팩토리 함수, 객체 생성
 
    std::tr1::shared_ptr<A> pB1(pA);  //최초 생성시 참조 카운트 1
    std::tr1::shared_ptr<A> pB2(pB1); //복사 -> 카운트 2
    std::tr1::shared_ptr<A> pB3 = pB1;//대입 -> 카운트 3
}

 

어떤 함수내에서 shared_ptr을 이용해서 객체를 사용하고 나면 여러개의 shared_ptr이 같은 객체를 참조 하고 있다하더라도 함수가 끝나면서 지역변수였던 모든 shared_ptr을 해제 하기 때문에 자연스럽게 동적할당된 자원이 반환되게 됩니다.

 

이렇듯, 스마트 포인터는 아주 중요한 부분이며, 자원관리에 효율적입니다.

하지만 이러한 스마트 포인터의 소멸자에 있는 delete 연산자는 delete [] 연산자가 아닙니다.

 

즉!

스마트 포인터 인자로 배열을 받으면 안된다는 것입니다.

 

std::auto_ptr<int> num( new int[10] ); // 문제가 발생합니다. 배열을 쓰면 안됩니다.

 

배열에 쓸수 있는 auto_ptr이라든지 tr1::shared_ptr을 원한다면, Boost라이브러리에 있는

boost::scoped_array와 boost::shared_array를 알아 보면 됩니다.

 

이것 만은 잊지 말자!

◆ 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용합시다.

◆ 일반적으로 널리 쓰이는 RAII 클래스는 tr1::shared_ptr 그리고 auto_ptr 입니다.

이 둘 가운데 tr1::shared_ptr이 복사 시의 동작이 직관적이기 때문에 대개 더 좋습니다. 반면, auto_ptr은 복사되는 객체(원본 객체)를 NULL로 만들어 버립니다.


설계가 잘 된 클래스들을 보면 객체를 복사하는 함수가 딱 둘만 있습니다.

 

복사생성자와 복사 대입 연산자 입니다. 이둘은 우리가 선언하지 않으면 컴파일러가 자동으로 만들어주는데, 깊은 복사가 되지 않습니다.

 

우리가 복사생성자와 복사 대입 연산자를 선언하여 깊은 복사를 해줘야 합니다.

 

이문제가 가장 크게 나타는 경우가 있는데, 예제를 보겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class B : public A{
    
    public:
      ...
      B(const B& rhs) : num(rhs.num) { //복사생성자
        LOG("COPY constructor");
      }    
      B& operator=( const B& rhs){ //복사 대입 연산자
        LOG("COPY operator");
        
        num = rhs.num;
        return *this;
      }
    
    private:
      int num;
};
 

 

위의 코드에 문제점은?

상속 관계에서 B의 복사생성자에는 기본클래스 A의 생성자에 넘길 인자들도 명시되어 있지 않아서 B객체의 A 부분은 인자 없이 실행되는 A 생성자, 즉 기본 생성자에 의해 초기화 됩니다. 

 

위와 같은 코드는 아래와 같이 바꾸셔야 합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class B : public A{
    
    public:
      ...
      B(const B& rhs) : A(rhs), num(rhs.num) { //복사생성자
        LOG("COPY constructor");
      }    
      B& operator=( const B& rhs){ //복사 대입 연산자
        LOG("COPY operator");
        
        A::operator=(rhs);
        num = rhs.num;
        return *this;
      }
    
    private:
      int num;
};
 

 

우리는 위의 코드처럼 두가지를 꼭 확인해야합니다.

 

1. 해당클래스의 데이터 멤버를 모두 복사합니다.

2. 이 클래스가 상속한 기본클래스의 복사 함수도 꼬박꼬박 호출해 주도록 합시다.

 

그리고

복사생성자와 복사 대입 연산자의 코드 중복은 양쪽에서 겹치는 부분을 별도의 멤버 함수에 분리해 놓은 후에 이함수를 호출하게 만드는게 좋습니다. ( Private 멤버로 init......() )

 

절대 복사 대입 연산자에서나, 복사 생성자에서 서로를 호출해서는 안됩니다.

 

이것 만은 잊지 말자!

◆ 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본클래스 부분을 빠뜨리지 말고 복사해야 합니다.

◆ 클래스의 복사 함수 두개를 구현할 때,  한쪽을 이용해서 다른쪽을 구현하려는 시도는 절대로 하지 마세요, 그대신, 공통된 동작을 제3의 함수에다가 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결 합시다.


자기 대입(self assignment)이란?

어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말합니다.

 

1
2
3
4
5
6
class Widget { ... };
 
Widget w;
...
 
w = w; // 자기에 대한 대입

 

위와 같은 구현을 하는 사람은 극히 드물겠지만, 예를 들어

 

a[i] = a[j]; 와 같이 i 및 j 가 같은 값을 갖게 되면 자기 대입문이 됩니다.

*px = *py; 와 같이 px 및 py가 가리키는 대상이 같으면 자기 대입문이 되고 맙니다.

 

위와 같이 이러한 자기대입이 생기는 이유는?

여러 곳에서 하나의 객체를 참조하는 상태, 다시말해 중복참조(aliasing)라고 불리는 것 때문입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Bitmap { ... };
 
class Widget {
    ...
    public:
     Widget& operator=(const Widget& rhs) {
        delete pb;                // 현재 bitmap 삭제
        pb = new Bitmap(*rhs.pb); 
        return *this;             
     }
    private:
     Bitmap* pb;
};
 
void main()
{
    Bitmap b;
    Widget w1, w2;        
    w1.pb = w2.pb = &b;
    // w1, w2 같은 비트맵을 가르키고 있다.
    w1 = w2;    
}

 

위의 구현의 문제점은?

같은 비트맵을 가르키게된 상태에서 대입 연산자가 호출되고, delete pb로 인해 두개의 w1, w2객체의 bitmap은 사라지게 됩니다. 그런데 이미 지워진 비트맵으로 pb를 생성하려 하니 예외가 발생하게 되는 것 입니다.

 

중요한것은

코드에서도 보면 아시겠지만, 하나의 객체를 여러곳에서 참조 하고 있는 상태에서 자기대입의 문제점을 말하고 있습니다.

 

위의 문제점은 객체가 같은지 즉 자기대입인지 검사문장(일치성테스트)으로 해결은 됩니다.

if( this == & rhs ) return *this;

 

말그대로, 자기대입이면, 아무것도 안하고 리턴하게끔 하는 것입니다.

하지만 위의 코드로는 자기대입에 대한 처리가 있을때의 예외처리만이 가능할뿐이지

if 문을 통과 하여 new 연산시 동적 할당에 필요한 메모리가 부족하다든지, 다양한 예외가 나오게 되면,  Widget객체는 결국 삭제된 Bitmap을 가리키는 포인터를 가지고 남습니다.

 

위와 같은 문제점을 해결하기 위해서는?

1
2
3
4
5
6
7
8
9
Widget& Widget::operator=(const Widget& rhs){
    
//  delete pb;  pb를 바로 삭제하지 않고, 복사본을 만든다.
    Bitmap * pOri = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOri;
    
    return *this;
}

 

위의 구현은 예외 안정성과, 자기대입 안정성을 동시에 가졌지만, 가장 효율적인 방법이라고는 할 수 없습니다.

 

사실상,

operator= 작성에 아주 자주 쓰이는 '복사 후 맞바꾸기(copy and swap) 기법을 들수 있습니다.

 

1
2
3
4
5
6
7
Widget& Widget::operator=(const Widget& rhs){
    
    Widget temp(rhs); // rhs의 데이터에 대해 사본을 하나 만듭니다.
    swap(temp);       // *this의 데이터를 그 사본의 것과 맞바꿉니다.
   
    return *this;
}

 

 

위의 코드처럼 구현하게되면,

대입되는 객체가 자기대입인지는 상관없고, 무조건 사본을 만들어 사본과 자신을 바꾸기때문에 자기 자신이면 그냥 Swap을 해도 자기자신이 되는거고, 아니라면 값이 바뀌는 것입니다.

 

또한,

rhs의 복사본 temp를 지역으로 선언 되어있다. 그리고 swap함수를 통해 this와 temp를 데이터를 바꾸게 되는데, 바꾼후 temp에는 this의 Bitmap포인터가 있을것이고, this는 rhs의 포인터로 바껴있을것이다. 이렇게 되면 자동적으로 블럭이 끝나는 순간 temp는 소멸되면서 말그대로 this의 Bitmap포인터가 소멸되는 것이다. 바꿔주면서 삭제가 자동이루어진 코드인 것이다.

 

예제중에서는 예외의 안정성과, 자기대입 안정성을 두루 갖춘 구현이라고 할 수 있겠습니다.

 

 

이것 만은 잊지 말자!

◆ operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만듭시다. 원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정 할 수도 있으며, 복사후 맞바꾸기 기법을 써도 됩니다.

◆ 두개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게ㅔ 동작하는지 확인해 보세요.



C++의 대입 연산은 여러 개가 사슬처럼 엮일 수 있는 재미있는 성질을 갖고 있습니다.

 

int x, y, z;

x = y = z = 15;
 
위의 구현은
일종의 관례이고, 클래스에 대입 연산자가 혹시 들어간다면 이 관례를 지키는 것이 좋습니다.
 
1
2
3
4
5
6
7
8
9
10
11
12
class Widget
{
    public:
     /**
      * (= 뿐만아니라 +=, -=, *=, /= 등 *this를 반환하도록 만든다.)
      */
    
     Widget& operator=(const Widget& rhs)
     {
        ...
        return *this;
     }
};
 
 
이관례는 모든 기본제공 타입들이 따르고 있을뿐만 아니라, 표준 라이브러리에 속한 모든  타입 ( string, vector, complex, tr1::shared_ptr 등) 에서도 따르고 있다는 점을 무시 못할 것입니다.
 
 
 이것 만은 잊지 말자!

◆ 대입 연산자는 *this의 참조자를 반환하도록 만드세요.


 

객체 생성 및 소멸 과정 중에는 가상함수를 호출하면 절대로 안 됩니다!!

우선 코드부터 봅시다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<iostream>
 
class Transaction
{
public:
Transaction();
virtual void logTransaction() const = 0;
};
Transaction :: Transaction()
{
      /**
* 생성자에서 순수가상함수를 호출했음
*/
logTransaction();
}
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
void BuyTransaction::logTransaction() const
{
}
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
void SellTransaction::logTransaction() const
{
}
int main()
{
BuyTransaction b;
return 0;
}






위의 코드는 다행스럽게도(?) LINK 에러를 내준다.

언뜻 봐서는 부모클래스의 순수가상함수를 자식들이 재정의 했고, BuyTransaction 선언으로

부모생성자가 호출되면서, 자식클래스의 정의함수로 넘어가게끔 되어 문제가 없어 보인다.

 

하지만,

이 코드가 LINK 에러를 내뱉는 이유는?

 

BuyTransaction 클래스는 Transaction 클래스를 상속받았기 때문에

상속받은 클래스의 객체가 생성될 때에는 부모 클래스의 생성자가 먼저 호출된다.

부모의 생성자가 먼저 호출된 상태에서 가상 함수를 호출하면, 현재 정의가 되지 않았기 때문에 오류가 생기게 된다.

 

즉!
부모의 생성자가 먼저 호출된다는 것은 그 동안에는 부모의 클래스 타입, 즉 Transaction 클
래스 타입으로 인식을 하게 된다는 것입니다.
그러므로, Transaction()함수의 정의는 찾을 수 없으니까 LINK에러가 나는 것입니다.

 

소멸자의 경우도 비슷하게 생각하면 된다. 

파생 클래스에서 정의된 소멸자가 수행되고, 부모 클래스의 소멸자가 수행이된다. 여기서 부모 클래스 소멸자가 수행될 때는 그 객체가 부모 클래스 타입이라고 인식하게 된다는 것이다.

 
위의코드가 에러가 나지 않는다면, 끔찍한 일(?)이 발생 합니다.
 
가장중요하게 우리가 알아야 할 부분은?
 
1. 생성자 및 소멸자에서는 가상함수를 호출 하면 안된다.
2. 파생 클래스의 생성자 및 소멸자 수행시 부모 클래스의 생성자/소멸자를 수행할 때, 컴파일러는 부모 클래스의 타입으로 인식하게 된다
 
 
그러면, 위의코드처럼 우리가 원하는건 파생클래스 객체 선언과 동시에 로그정보를 호출되기를 원한다고 가정 해봅시다. 방법은 많이 있겠지만, 책의 나와 있는 방법으로 간단하게 해결하도록 하겠습니다.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<iostream>
 
class Transaction 
  public
    explicit Transaction(const std::string& logInfo);
    void logTransaction(const std::string& logInfo) const;   
};
Transaction::Transaction(const std::string& logInfo)
    logTransaction(logInfo); 
}
void Transaction::logTransaction(const std::string& logInfo) const
{
    std::cout<<logInfo.c_str()<<std::endl;
}
 
class BuyTransaction : public Transaction 
public:
    BuyTransaction(const char * log) : Transaction(createLogString(log)){ }
private:
    static std::string createLogString(const char * log) { return log; }
}; 
 
int main() 
    BuyTransaction b("TEST");
    return 0;
}
 
 
위의 코드는 순수가상함수였던 (logTransaction) 함수를 비가상함수로 바꾸고, 자식클래스 생성자들로 하여금 필요한 로그정보를 부모생성자로 넘기고 있습니다.  
 
 

이것 만은 잊지 말자!

◆ 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요. 가상 함수라고 해도, 지금 실행 중인 생성자나 소멸자에 해당된느 클래스의 파생클래스 쪽으로는 내려가지 않으니까요

 


 

class Widget {}

 

vector<Widget> v;

위의 클래스가 선언되어 있고, vector<Widget> v의 사이즈가 10이라고 가정합니다.

 

v가 사이즈가 10인데, 첫번째 것을 소멸시키는 도중에 예외가 발생되었다고 가정하면, 나머지 아홉 개는 소멸자가 호출되지 않아 메모리 누수가 생깁니다.

 

위의 문제를 방지하기 위해서는 소멸자에게 예외가 나오는 것을 방지해야 합니다.

예를 들어봅시다.

 

ex ) DB를 접속하는 클래스

class DBConnection {

  public:

  /**

   * 생성자 소멸자가 아닌 따로 함수를 만들어서 생성 소멸을 할 수 있게 작업

   */

  static DBConnection create();

  void close();

};

 

ex) DBConnetion에 대한 자원 관리 클래스

class DBConn {

public :

 ~DBConn()

  {

      db.close();

   }

private:
  DBConnection db;
};
 
int main()
{
  /**
  * DBConnection 객체를 생성하고 이것을 DBConn 객체로 넘겨서 관리를 맡깁니다.
  * DBConn 인터페이스를 통해 DBConnection 객체를 사용합니다.
  * main함수 종료시 DBConn 객체가 소멸됩니다. 따라서 DBConnection 객체에 대한 close
  * 함수의 호출이 자동으로 이루어 집니다.
  */
  DBConn dbc ( DBConnection::create() );
  return 0;
}
 
위의 코드처럼
사용자의 망각을 사전에 차단하는 DBConnection에 대한 자원 관리 클래스를 만들어서 그 클래스의 소멸자에서 close를 호출하게 만드는 것입니다.
 
위와 같은 방법은 close() 함수가 제대로 호출만 된다면 문제 없다.
하지만 close() 함수를 호출했는데, 여기서 예외가 발생한다면, 또 문제가 생기게 된다.
 
이것을 피하기 위한 방법은 두가지가 있다.
 
첫째. close에서 예외가 발생하면 프로그램을 바로 끝냅니다. 대개 abort를 호출합니다.
 
DBConn::~DBConn()
{
    try { db.close(); }
    catch(...)
   {
       // close 호출이 실패했다는 로그를 작성합니다.
       std::abort();
   }
 
try ~ catch 문을 이용하여 일단 close() 함수를 실행한 뒤, 예외가 발생시 abort() 함수를 이용하여
프로그램을 끝내버린다.
 

객체 소멸이 진행되다가 에러가 발생한 후 더이상 프로그램을 실행할 수 없는 상황일 경우에 사용한다.

둘째. close를 홀출한 곳에서 일어난 예외를 삼켜버립니다. 

DBConn::~DBConn()
{
    try { db.close(); }
    catch(...)
    {
       // close 호출이 실패했다는 로그를 작성합니다.     
     }
}
 
예외 발생시 catch 블럭으로 들어가긴 하지만 아무것도 수행하지 않는다. 즉, 예외를 삼킨 것이다.
 

예외 삼키기를 선택한 것이 제대로 빛을 보려면, 발생한 예외를 그냥 무시한 뒤라도 프로그램이 신뢰성 있게 실행을 지속할 수 있어야 합니다.

 

이 두가지 방법 또한 문제점들을 가지고 있습니다.

 

이유는?

이 두 방법은 예외가 발생한 후의 처리를 하는 것인데, 실제로 close()가 예외를 던지게 된 요인에 대

한 조치를 취하는 대책이 전무한 상태입니다.

 

더 나은 방법은 아래와 같습니다. 

DBConn 클래스에서 close()를 직접 제공하는 방법이 있습니다.

 

이렇게 만들면, close() 함수가 실행 중에 발생하는 예외를 사용자가 처리할 수 있습니다.

 

class DBConn {

public:

   ...

   void close()              // 사용자에게 호출을 위해 정의한 close() 함수

  {

db.close();

closed = true; 

   }

   ~DBConn()

  {

if( !closed )

{

    try{

      db.close();

    }

    catch( ... )

   {

       // close 호출 실패 로그 출력

       // ...

   }

}

   }

 

private:

DBConnection db;

bool closed;

};

 

close() 호출 책임을 소멸자에서 사용자에게로 떠넘긴다.

예외를 처리할 수 있는 기회를 사용자에게 주는 것입니다. 이것 마저 없다면 사용자는 예외에 대처할 기회를 못잡게 됩니다. ( 2차 검증으로 DBConn 소멸자에서 마무리 )

 

 

즉!

예외가 생길 수 있는 코드는 소멸자보다는 다른 함수에서 비롯되어야 한다.

 

이것 만은 잊지 말자!

◆ 소멸자에서는 예외가 빠져나가면 안됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야합니다.

◆ 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 합니다.


+ Recent posts