성공할 게임개발자

[Effective c++] [8. new와 delete를 내 맘대로] 49. new 처리자의 동작 원리를 제대로 이해하자 본문

C++

[Effective c++] [8. new와 delete를 내 맘대로] 49. new 처리자의 동작 원리를 제대로 이해하자

fn000 2025. 7. 2. 11:34

new 처리자(new-handler 할당에러 처리자)

메모리 할당 요청을 operator new함수가 맞추어 주지 못할 경우 사용자 쪽에서 지정할 수 있는 에러 처리 함수를 우선적으로 호출하도록 하는 함수

namespace std
{
    typedef void(*new_handler)();
    
    new_handler set_new_handler(new_handler p) throw();
}

 

 

typedef void(*new_handler)()  의 의미 new_handler는 인자없고 void 반환형인 함수 포인터 타입의 별명

즉, void 반환, 인자 없음 형태의 함수 포인터 타입을 new_handler 라는 이름으로 정의한다의 뜻

 

위에서 set_new_handler는 new_handler를 받고 new_handler를 반환하는 함수.

set_new_handler가 받아들이는 new_handler 타입의 매개변수는 operator new가 할당하지 못했을 때 호출할 함수의 포인터이다

 

void outOfMem()
{
   std::cerr << "Unable to satisfy request for memory";
   std::abort();
}

int main()
{
   std::set_new_handler(outOfMem);
   int *bigDataArray = new int[100000000L];
   ...
}

new_handler 를 typedef로 정의했으므로(void 타입, 인자없음) set_new_handler에 인자로 넣을 수 있다.

 

이 함수는 메모리가 부족하여 할당 실패 시 에러 메세지를 출력하는데 만약 에러메세지를 출력하는데 메모리가 동적으로 할당 되어야 한다면? (예를 들면, 벌금을 내야하기위해 벌금을 내는 느낌?) 이 경우 operator new는 충분한 메모리를 찾아낼 때까지 new를 뒤풀이해서 호출한다. 이게 좋게 설계되어있을라면 아래 동작 중 하나를 꼭 해주어야 한다.

 

1. 사용할 수 잇는 메모리를 더 많이 확보한다

구현방법 중 하나는 프로그램이 시작할 때 메모리 블록을 크게 하나 할당해 놓았다가 new처리자가 가장 처음 호출될 때 그 메모리를 쓸 수 있게 하는 전략

 

2. 다른 new처리자를 설치

자기 몫을 해줄 다른 new 처리자를 설치한다.(new 처리자 안에서 set_new_handler 호출) operator new 가 다시 new 처리자를 호출할 때, 새로 설치된  new처리자가 호출되는 것. 혹은 new처리자가 자기 자신을 다른 방식을 동작하게 동작원리를 바꾼다.

 

3. new처리자의 설치를 제거

set_new_handler에 널 포인터를 넘김. 

 

4.예외를 던짐

 

5. 복귀하지 않는다

대개 abort, exit 을 호출

 

 

C++에는 특정 클래스만 을 위한 할당 에러 처리자를 둘 수 잇ㄴ느 기능이 없다.

 

그냥 클래스에서 구현하면 된다 자체버전의 set_new_handler, operator new

class Widget
{
public:
    static new_handler set_new_handler(new_handler p) throw();
    static void* operator new(size_t size) throw(bad_alloc);
    
private:
    static new_handler currentHandler;
};

new_handler Widget::currentHandler = 0; // 클래스 구현 파일에서

new_handler Widget::set_new_handler(new_handler p) throw()
{
    new_hnadler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

전역 operator new를 호출하여 실제 메모리 할당을 수행한다. 전역 operator new할당이 실패하면 이 함수는 Widget의 new처리자를 호출한다

 

자원 관리 클래스를 이용하여 RAII 연산

class NewHandlerHolder
{
public:
    explicit NewHandlerHolder(new_handler nh) : handler(nh) {}
    
    ~NewHandlerHolder()
    { set_new_handler(handler); }
    
private:
    new_handler handler;
    
    NewHandlerHolder(const NewHandlerHolder&);
    NewHandlerHolder& operator=(const NewHandlerHolder&);
 };
 
 
 
 
 
 temaplate<typename T>
void* NewHandlerSupport<T>::operator new(size_t size) throw(bad_alloc)
{
    NewHandlerHolder h(set_new_handler(currentHandler));
    return ::operator new(size);
}

// widget의 operator new 구현
void* Widget::operator new(size_t size) throw(bad_alloc)
 {
     NewHandlerHolder h(set_new_handler(currentHandler));
        
     return ::operator new(size);
 }
 
 // widget 객체에 대한 메모리할당 실패했을 때, 호출되는 함수
 void outOfMem();
 
 // new처리자 함수, outOfMem 설치
 Widget::set_new_handler(outOfMem);
 
 // 메모리 할당이 실패하면 outofMem이 호출
 Widget* pw1 = new Widget;
 
 // 할당 실패 시 전역new처리자 함수 호출
 string* ps = new string;
 
 //Widget 클래스만을 위한 new처리자 함수가 없도록 null로 설정
 Widget::set_new_handler(0);
 
 //이제 실패 시 바로 예외를 던짐 new처리자 함수 null
 Widget* pw2 = new Widget;

 이 코드를 다른 클래스에서도 재사용 하기위해 믹스인 양식의 기본 클래스를 쓴다. 즉, 다른 파생 클래스들이 한 가지의 특정 기능만을 물려받아 갈 수 있도록 설계된 기본 클래스를 만들면된다.

 

지금의 경우 특정기능은  클래스별 new처리자를 설정한는 기능이고 이것이 포함된 기본 클래스를 템플릿으로 탈바꿈 시킨다.

 

 

// 클래스별 set_new_handler를 지원한느 "믹스인 양식"의 기본 클래스
template<typename T>
class NewHandlerSupport
{
public:
    static new_handler set_new_handler(new_handler p) throw ();
    static void* operator new(size_t size) throw(bad_alloc);
    
    ...
    
private:
    static new_handler currentHandler;
};




template<typename T>
new_handler
NewHandlerSupport<T>::set_new_handler(new_handler p) throw()
{
    new_handler oldHandler = currentHandler;
    currentHandelr = p;
    return oldHandler;
}

temaplate<typename T>
void* NewHandlerSupport<T>::operator new(size_t size) throw(bad_alloc)
{
    NewHandlerHolder h(set_new_handler(currentHandler));
    return ::operator new(size);
}


// 클래스 별로 만들어지는 currentHandler 멤버를 널로 초기화한다
template<typename T>
new_handler NewHandlerSupport<T>::currentHandler = 0;

class Widget : public NewHandlerSupport<Widget>
{
	...
};

 

NewHandlerSupport<Widget> 으로부터 Widget이 상속받는 것을 신기하게 반복되는 템플릿패턴 CRTP라고 한다.

 

 

 

1993년까진 operator new가 메모리 할당을 할 수 없을 때 널 포인터를 반환하라고 했지만 이제는 bad_alloc 예외를 던지도록 명세가 바뀐다. 하지만 이걸 포기하기 싫었던 C++표준화 위원회는 전통적인 할당-실패-시-널-반환 으로 동작하는 형태의 예외불가 형태를 내놓았다.

 

class Widget { ... };


// 할당 실패 시 bad_alloc 예외 던짐
Widget* pw1 = new Widget;

// 그러면 이 코드는 반드시 실패
if (pw1 == 0) ...

// 할당하다 실패하면 널 반환
Widget* pw2 = new (nothrow) Widget;

//이 점검 코드는 성공할 수 도 있음
if (pw2 == 0) ...

예외불가 new는 그때 호출되는 operator new 에서만 예외가 발생되지 않도록 보장할 뿐, new (std::nothrow) widget등의 표현식에서 예외가 나오지 않게 막아준다는 이야기는 아니다.

 

 

중요포인트!

set_new_handler 함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출되는 함수를 지정할 수 있다.

 

예외불가(nothrow) new는 영향력이 제한되어 있다. 메모리 할당 자체만 적용되기 때문. 이후 호출되는 생성자에서는 얼마든지 예외를 던질 수 있음