49: new 처리자의 동작 원리를 제대로 이해하자
메모리 할당이 제대로 되지 못한 상황에 대한 반응으로 operator new가 예외를 던지기 전에, 이 함수가 사용자 쪽에서 지정할 수 있는 에러 처리 함수를 우선적으로 호출하도록 되어 있는데, 이 에러 처리 함수를 가리켜 new 처리자(new-handler)라고 한다. 표준 라이브러리에는 set_new_handler라는 함수가있다.
void outOfMem()
{
std::cerr << "Unalbe to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int* pBigDataArray = new int[100000000000];
}
매개변수는 요구된 메모리를 operator new가 할당하지 못했을 때 operator new가 호출할 함수의 포인터이다.
operator new는 충분한 메모리를 찾아낼 때까지 new 처리자를 되풀이해서 호출한다. new 처리자 함수가 프로그램 동작에 좋은 영향을 미치는 쪽으로 설계되어 있다면 다음 동작 중 하나를 꼭 해주어야 한다.
- 사용할 수 있는 메모리를 더 많이 확보한다.
- operator new가 시도하는 이후의 메모리 확보가 성공할 수 있도록 하자는 전략
- 다른 new 처리자를 설치한다.
- 다른 new 처리자의 존재를 알고 있을 가능성이 있을 때
- new 처리자의 설치를 제거한다.
- set_new_handler에 널 포인터를 넘긴다.
- 예외를 던진다.
- bad_alloc 혹은 bad_alloc에서 파생된 타입의 예외를 던진다.
- 복귀하지 않는다.
- abort혹은 exit를 호출
클래스 타입에 따라서 메모리 할당 실패에 대한 처리를 다르게 가져가고 싶을 때는 어떻게 할까? 해당 클래스에서 자체 버전의 set_new_handler 및 operator new를 제공하도록 만들어 주면 된다.
operator new가 충분한 메모리를 할당하지 못할 경우에 호출될 new 처리자 함수를 어딘가에 간수해 둘 필요가 있으므로, 이 new 처리자를 가리키는 new_handler 타입의 정적 멤버 데이터를 선언한다.
class Widget {
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
std::new_handler Widget::currentHandler = 0; // 널로 초기화, 클래스 구현 파일에 두어야 한다.
set_new_handler 함수는 자신에게 넘어온 포인터를 아무런 점검없이 저장해 놓고, 바로 전에 저장했던 포인터를 역시 아무런 점검 없이 반환하는 역할만 맡는다.
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
이제 operator new가 할 일만 남았다.
- 전역 new 처리자로서 Widget의 new 처리자를 설치한다.
- 전역 operator new를 호출하여 실제 메모리 할당을 수행한다. 전역 operator new의 할당이 실패하면, 이 함수는 Widget의 new 처리자를 호출하게 된다. 마지막까지 전역 operator new의 메모리 할당 시도가 실패하면, 전역 operator new는 bad_alloc 예외를 던진다. 이 경우 Widget의 operator new는 전역 new 처리자를 원래의 것으로 되돌려 놓고, 이 예외를 전파시켜야 한다. 자원 관리 객체를 사용하여 전역 new 처리자를 관리함으로써 자원 누수를 막는다.
- 전역 operator new가 Widget 객체 하나만큼의 메모리를 할당할 수 있으면, Widget 의 operator new는 이렇게 할당된 메모리를 반환한다. 이와 동시에, 전역 new 처리자를 관리하는 객체의 소멸자가 호출되면서 Widget의 operator new가 호출되기 전에 쓰이고 있던 전역 new 처리자가 자동으로 복원된다.
// 자원 관리 객체
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh)
: handler(nh) {}
~NewHandlerHolder()
{
std::set_new_handler(handler);
}
private:
std::new_handler handler;
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
// Wiget의 new 처리자를 설치한다.
NewHandlerHolder h(std::set_new_handler(currentHandler));
// 메모리를 할당하거나 할당이 실패하면 예외를 던진다.
return ::operator new(size);
}
// 이전의 전역 new 처리자가 자동으로 복원된다.
Widget 클래스를 사용하는 쪽에서 new 처리자 기능을 쓰려면 다음과 같이 하면된다.
void outOfMem();
Widget::set_new_handler(outOfMem);
Widget* pw1 = new Widget;
std::string* ps = new std::string;
Widget::set_new_handler(0);
이 코드를 다른 클래스에서도 재사용할 수 있도록 믹스인 양식의 기본 클래스를 추천하고 싶다. 즉, 다른 파생 클래스들이 한 가지의 특정 기능만을 물려받아 갈 수 있도록 설계된 기본 클래스를 만들면 된다.
template<typename T>
class NewHandlerSupport {
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
std::new_handler currentHandler;
};
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
이 템플릿의 매개변수인 T는 그냥 파생 클래스들을 구분해 주는 역할만 한다. 템플릿 메커니즘 자체는 NewHandlerSupport가 인스턴스화될 때 전달되는 T를 위한 currentHandler의 사본을 자동으로 찍어내는 공장인 셈이다. 이런 패턴을 신기하게 반복되는 템플릿 패턴(curiously recurring template pattern: CRTP)이라고 부른다. 책의 필자는 "나만의 것(Do It For Me)"라는 이름을 쓴다.
이것만은 잊지 말자!
- set_new_handler 함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출되는 함수를 지정할 수 있다.
- 예외불가(nothrow) new는 영향력이 제한되어 있다. 메모리 할당 자체에만 적용되기 때문이다. 이후에 호출되는 생성자에서는 얼마든지 예외를 던질 수 있다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 (0) | 2023.11.22 |
---|---|
[Effective C++] 50: new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자 (1) | 2023.11.21 |
[Effective C++] 48: 템플릿 메타프로그래밍, 하지 않겠는가? (0) | 2023.11.18 |
[Effective C++] 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 (0) | 2023.11.18 |
[Effective C++] 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 (0) | 2023.11.16 |