KoreanFoodie's Study
Effective C++ | 항목 49 : new 처리자의 동작 원리를 제대로 이해하자 본문
Effective C++ | 항목 49 : new 처리자의 동작 원리를 제대로 이해하자
GoldGiver 2022. 10. 25. 16:33
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 49 : new 처리자의 동작 원리를 제대로 이해하자
핵심 :
1. set_new_handler 함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출되는 함수를 지정할 수 있다.
2. 예외불가(nothrow) new 는 영향력이 제한되어 있다. 메모리 할당 자체에만 적용되기 때문이다. 이후에 호출되는 생성자에서는 얼마든지 예외를 던질 수 있다.
메모리 할당이 제대로 되지 못했을 때, operator new 는 예외를 던지기 전 사용자 쪽에서 지정할 수 있는 에러 처리 함수를 우선적으로 호출한다. 이 함수를 new 처리자(new-handler, 할당에러 처리자) 라고 한다. 이와 같은 함수를 사용자 쪽에서 지정할 수 있도록, 표준 라이브러리에 set_new_handler 라는 함수가 준비되어 있다.
namespace std
{
typedef void (*new_handler)();
// throw() 는 예외지정으로, 예외를 던지지 않는다
new_handler set_new_handler(new_handler p) throw();
}
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
// Unable to satisfy request for memory 출력!
int *pBigDataArray = new int[100000000000000l];
}
위에서 new_handler 의 타입은 void 를 받아 void 를 리턴하는 함수에 대한 포인터이다.
호출되는 new 처리자 함수를 만들 때는 다음 사항들을 고려해서 만들어야 한다.
- 사용할 수 있는 메모리를 더 많이 확보한다
- 다른 new 처리자를 설치한다 : 현재의 new 처리자 안에서 set_new_handler 를 설치하고 호출한다
- new 처리자의 설치를 제거한다 : set_new_handler 에 널 포인터를 넘긴다
- 예외를 던진다
- 복귀하지 않는다 : 대개 abort 혹은 exit 을 호출한다
특정 클래스의 메모리 할당이 실패했을 때, 우리가 원하는 함수가 호출되길 원한다면 이런 식으로 구현하면 된다.
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;
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
그 후 Widget 의 operator new 를 다음과 같이 동작하게 한다.
- 표준 set_new_handler 함수에 Widget 의 new 처리자를 넘겨서 설치(전역 new 처리자로서 Widget 의 new 처리자를 설치) 한다.
- 전역 operator new 를 호출하여 실제 메모리 할당을 수행한다. 실패하면, 이 함수는 Widget 의 new 처리자를 호출한다(전 단계에서 처리자 설치했음). 마지막까지 전역 operator new 의 메모리 할당 시도가 실패하면, bad_alloc 예외를 던진다. 이 경우 Widget 의 operator new 는 전역 new 처리자를 원래의 것으로 되돌려 놓고 이 예외를 전파한다. 이때 전역 new 처리자를 자원으로 간주하고 처리한다.
- 전역 operator new 가 Widget 객체 메모리 할당에 성공하면, 전역 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;
// 복사를 막기 위한 부분 (항목 14 참고)
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
// Widget 의 new 처리자 설치
// NewHandlerHolder 는 currentHandler 로 핸들러가
// 바뀌기 전의 oldHandler 를 쥐고 있음!
NewHandlerHolder h(std::set_new_handler(currentHandler));
// 할당 실패 시 예외를 던짐
return ::operator new(size);
// 이전의 전역 new 처리자가 자동으로 복원됨
}
int main()
{
// Widget 객체에 대한 메모리 할당이 실패했을 때 호출할 함수
void outOfMem();
// Widget 의 new 처리자 함수로 outOfMem 설치
Widget::set_new_handler(outOfMem);
// 메모리 할당 실패 시 outOfMem 호출
Widget *pw1 = new Widget;
// 메모리 할당 실패 시 전역 new 처리자 함수 호출(있으면)
std::string *ps = new std::string;
// Widget 클래스의 new 처리자 함수를 null 로 설정
Widget::set_new_handler(0);
// 메모리 할당이 실패하면 이제 바로 예외를 던짐
Widget *pw2 = new Widget;
}
위의 코드를 템플릿화하여 다른 클래스들도 자유롭게 사용하도록 만들어 보자.
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);
// operator new 의 다른 버전들을 아래에 구현 (항목 52 참고)
private:
static 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;
이제 Widget 은 NewHandlerSupport<Widget> 을 상속받기만 하면 된다.
class Widget: public NewHandlerSupport<Widget>
{
public:
// int arr[10000000000000000l];
private:
static std::new_handler currentHandler;
};
위의 코드처럼, Widget 이 NewHandlerSupport<Widget> 으로부터 상속받는 패턴을 신기하게 반복되는 템플릿 패턴(curiously recurring template pattern: CRTP) 라고 부른다.
추가적으로 한 가지만 짚고 넘어가자. 1993 년까지, C++ 는 operator new 가 메모리 할당에 실패하면 널 포인터를 반환했지만, 이후 bad_alloc 예외를 던지도록 명세를 바꾸었다. 과거의 동작을 지원하기 위해, "예외불가(nothrow)" 형태의 operator new 를 새롭게 만들었다.
class Widget { ... };
// 할당이 실패하면 bad_alloc 예외를 던짐
Widget *pw1 = new Widget;
if (pw1 == 0) {} // 이 점검 코드는 항상 실패
// 할당이 실패하면 0(null) 을 반환
Widget *pw2 = new (std::nothrow) Widget;
if (pw2 == 0) {} // 이 점검 코드는 성공할 수 있음
하지만, new (std::nothrow) Widget 에서는 예외가 발생하지 않아도, Widget 의 생성자에서 예외가 발생하면 예전처럼 예외가 전파된다. 아마 예외불가 new 를 필요로 할 일은 거의 없을 것이다!
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective C++ | 항목 51 : new 및 delete 를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 (0) | 2022.10.26 |
---|---|
Effective C++ | 항목 50 : new 및 delete 를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자 (0) | 2022.10.25 |
Effective C++ | 항목 48 : 템플릿 메타프로그래밍, 하지 않겠는가? (0) | 2022.10.25 |
Effective C++ | 항목 47 : 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 (0) | 2022.10.25 |
Effective C++ | 항목 46 : 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 (0) | 2022.10.25 |