본문 바로가기
프로그래밍언어/C++

[Effective C++] 항목 13 ~ 17

by 목가 2018. 1. 23.
반응형

항목13. 자원 관리에는 객체가 그만!


Class Investment {...};     // 투자를 모델링한 최상위 클래스
Investment * createInvestment     // Investment 클래스의 객체를 얻는 팩토리 함수

void f()
{
Investment *pInv = createInvestment();     //팩토리 함수 호출
...
delete pInv;
}

위 상황에서 여러가지 경우의 수에 의해 delete 가 호출되지 않을 가능성이 존재한다. (return, continue, goto, exception등)
해결 방법 : 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며, 그 소멸자는 실행 제어가 함수를 떠날 때 호출되도록 만드는 것 (스마트 포인터 사용)

void f()
{
std:auto_ptr<Investment> pInv(createInvestment());     // 팩토리 함수 호출
...
}                                                                     // 소멸자를 통해 pInv 삭제

자원 관리에 객체를 사용하는 중요한 두 가지 특징
1. 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
 - 자원 획득 즉 초기화(Resource Acquisition is Initialization: RAII)
2. 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다. 

auto_ptr 의 문제
 - 어떤 객체를 가리키는 auto_ptr 의 개수가 둘 이상이면 안됨.
 - auto_ptr 객체를 복사하면 원본 객체는 null 로 만들어 버림.

std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1);     // pInv2는 이전에 pInv1 이 가리키던 객체를 가리키는 반면 pInv1 는 null 이 됨

pInv1 = pInv2;    // pInv1 는 pInv2 이 가리키던 객체를 가리키고 pInv2 는 null 이 됨

위의 대안으로 참조 카운팅 방식 스마트포인터(reference-counting smart pointer: RCSP)를 사용.
void f()
{
...
std::tr1::shared_ptr<Investment> pInv(createInvestment());     // 팩토리 함수 호출
...                                                                   //pInv 사용
}                                                                            //shared_ptr 의 소멸자를 통해 pInv 를 자동 삭제

void f()
{
std::tr1::shared_ptr<Investment> pInv1(createInvestment());      
std::tr1::shared_ptr<Investment> pInv2(pInv1);                  // pInv1 및 pInv2 가 동시에 같은 객체를 가리킴 
pInv1 = pInv2;                                                      // 마찬가지 변한것은 없음
...                                                                   
}                                                                            // pInv1 및 pInv2는 소멸되며 이들이 가리키는 객체도 삭제

스마트 포인터는 소멸자 내에서 delete 연산자를 사용한다. delete [] 가 아니므로 동적으로 할당한 배열에 대해 auto_ptr 이나 shared_ptr 을 사용하면 안된다. 이런 경우 boost 에서 제공하는 boost::scoped_array, boost::shared_array 가 있다

* 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해결하는 RAII 객체를 사용해라
* RAII 클래스 중 tr1::shared_ptr 이 복사시에 동작이 직관적이기 때문에 대개 더 좋다. auto_ptr 은 복사되는 객체는 null 로 만듦

항목 14. 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자. 

Lock ml1 (&m);     // m에 잠금을 건다.
Lock ml2 (ml1);    // ml1 을 ml2 로 복사한다. 어떻게 되어야 할까?

1. 복사를 금지한다. 어떤 스레드 동기화 객체에 대한 '사본'이라는게 실제로 거의 의미가 없다.
class Lock : private Uncopyable {    // 복사를 금지함
public:
...
}
2. 관리하고 있는 자원에 대해 참조 카운팅을 수행
일반 포인터 대신에 스마트 포인터를 사용한다. 단 참조 카운팅이 0이 되면 삭제가 되버리기 때문에 의도한 동작과는 다르게되므로 삭제자를 unlock 함수로 지정해서 해제만 하도록 한다.그래서 아래 클래스에는 소멸자가 따로 존재하지 않는다. 
class Lock{
public:
explicit Lock(Mutex *pm)     // shared_ptr 을 초기화 하는데, 가리킬 포인터로 Mutex 객체의 포인터를 사용하고
: mutexPtr(pm, unlock)      // 삭제자로 unlock 함수를 사용
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
3. 관리하고 있는 자원을 진짜로 복사
 - 자원 관리 객체를 복사하면 그 객체가 둘러싸고 있는 자원까지 복하되어야 한다. (깊은 복사)
4. 관리하고 있는 자원의 소유권은 옮긴다.
 - auto_ptr 의 복사과정 처럼

* RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정된다.
* RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해주는 선으로 마무리해라. 

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

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

int daysHeld(const Investment *pi);
int days = daysHeld(pInv);                // 컴파일 에러

daysHeld 함수는 Investment* 타입의 실제 포인터를 원하는데 스마트 포인터 타입의 객체를 넘기는 문제발생
shared_ptr, auto_ptr 은 명시적 변환을 수행하는 get 멤버함수를 제공

int days = daysHeld(pInv.get());       // 문제없음. 실제 포인터를 daysHeld 에 넘김

스마트 포인터는 역참조 연산자(operator-> 및 operator*)도 오버로딩하고 있으므로 실제 포인터에 대한 암시적 변환도 쉽게 할 수 있다. 

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

항목 16. new 및 delete 를 사용할 때는 형태를 반드시 맞추자.

std::string *stringArray = new std::string[100];
...
delete stringArray;
100개의 string 객체들 가운데 99개는 정상적인 소멸을 거치지 못할 가능성이 크다. 
delete 연산자가 적용되는 객체의 개수는 소멸자가 호출되는 횟수이다. 

std::string *stringPtr1 = new std::string;
std::string *stringPtr2 = new std::string[100];
...
delete stringPtr1;
delete[] stringPtr2;

typedef 로 정의된 어떤 타입의 객체를 메모리에 생성하려고 new 를 썼을 때 나중에 어떤 형태의 delete 를 쓸지는 작성자가 책임져야 한다. 따라서 배열타입을 typedef 로 만들지 않는 것이 좋다. 

typedef std::string Addresslines[4];
std::string *pal = new AddressLines;    // new string[4] 와 마찬가지
delete [] pal;        // 배열 객체이므로 delete [] 로 삭제

* new 표현식에 []를 썻으면 대응되는 delete 표현식에도 [] 를 써야한다. 반대도 마찬가지.

항목 17. new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자.

int priority();
void processWidget(std::tr1:shared_ptr<Widget> pw, int priority);

processWidget(new Widget, priority());

위 함수를 호출하면 컴파일 에러 발생한다. tr1::shared_ptr 의 생성자는 explicit 으로 선언되어 있기 때문에 'new Widget' 표현식에 의해 만들어진 포인터가 tr1::shared_ptr 타입의 객체로 바꾸는 암시적인 변환이 없다. 

processWidget(std::tr1:shared_ptr<Widget>(new Widget), priority());

컴파일은 되지만 여기서 문제가 발생할 수 있음. 컴파일러는 processWiget 호출 코드를 만들기 전에 이 함수의 매개변수로 넘겨 받는 인자를 평가하는 단계를 거친다. 첫 번째 인자(std::tr1:shared_ptr<Widget>(new Widget))가 두 부분으로 나누어져 있음.

 - new Widget 을 실행하는 부분
 - tr1::shared_ptr 생성자를 호출하는 부분

따라서 호출되는 아래의 세 가지 연산이 호출된다. 
 - 'new Widget' 실행
 - tr1::shared_ptr 생성자를 호출
 - priority 호출

또한 컴파일러 제작사마다 각각의 연산의 실행되는 순서가 다르다. 만약 priotiy 함수에서 예외가 발생한다고 했을 때, new Widget 이후에 예외가 발생하면 자원 누출이 생기게 된다. 
해결 방법 : Widget 을 생성해서 스마트 포인터에 저장하는 코드를 별도의 문장 하나로 만들고, 그 스마트 포인터를 processWidegt 함수에 넘기는 것
std::tr1:;shared_ptr<Widget> pw(new Widget);   // new 로 생성한 객체를 스마트 포인터에 담는 코드를 하나의 문장으로
processwidget(pw, priority());

* new 로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장으로 만들자. 이것이 안 되어 있으면, 에외가 발생될 때 디버깅하기 힘든 자원 누출이 초래될 수 있다. 


반응형

'프로그래밍언어 > C++' 카테고리의 다른 글

[Effective C++] 항목 26~28  (0) 2018.04.10
[Effective C++] 항목 22 ~ 25  (0) 2018.01.23
[Effective C++] 항목 18 ~ 21  (0) 2018.01.23
[Effective C++] 항목 5 ~ 12  (0) 2018.01.23
[Effective C++] 항목1 ~ 4  (0) 2018.01.23

댓글