728x90
반응형

메모리 할당 과정

operator new 메모리 할당이라는 임무를 맡고 있으며, 임무 실패 시 예외를 발생시킨다.

하지만, 예외를 던지기 전 사용자 에러 처리 함수를 선 호출하도록 설계되어있는데, 이 에러 처리 함수를 가리켜  new 처리자(new-handler,  할당 에러 처리자)라고 한다.

이와 같은 메모리 고갈 상황을 처리할 함수를 사용자 쪽에서 지정할 수 있도록 표준 라이브러리에서는 set_new_handler라는 함수가 준비되어있다. 이 함수는 <new>에 선언되어 있다.

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

new_handler는 받는 것도 없고 반환하는 것도 없는 함수의 포인터에 대해 typedef를 걸어놓은 타입 동의어이다. 그리고 set_new_handler는 new_handler를 받고 new_handler를 반환하는 함수이다.

new_handler는 요구된 메모리를 operator new가 할당하지 못했을 때 operator new 가 호출할 함수의 포인터이며, 반환 값은 set_new_handler가 호출되기 전까지 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];
}

operator new가 1억 개의 정수 할당에 실패하면 outOfMem 함수가 호출되고, 이 함수는 에러 메시지를 출력하면서 프로그램을 강제로 끝낼 것이다.

그런데 cerr에 에러 메시지를 쓰는 과정에서 또 메모리가 동적으로 할당되어야 한다면? 사용자가 부탁한 만큼의 메모리를 할당해 주지 못하면 operator new는 충분 한 메모리를 찾아낼 때까지 new 처리자를 되풀이해서 호출할 것이다.

 

어쨌든 이를 통해 호출되는 new 처리자 함수가 프로그램의 동작에 좋은 영향을 미치는 쪽으로 설계되어 있다면 다음 동작 중 하나를 꼭 해주어야 한다.

 

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

operator new가 시도하는 이후의 메모리 확보가 성공할 수 있도록 하자는 전략이다. 구현 방법은 여러 가지가 있지만, 프로그램이 시작할 때 메모리 블록을 크게 할당해 놓았다가 new처리자가 첫 호출(메모리 할당 첫 실패 시) 될 메모리를 쓸 수 있도록 하는 방법이 그 한 가지이다.

 

2. 다른 new 처리자를 설치한다.

현재의 new 처리자가 더 이상 가용 메모리를 확보할 수 없다고 해도, 자기 몫까지 해줄 다른 new 처리자의 존재를 알고 있을 가능성이 있다.

이런 상황에서 현재의 new 처리자는 제자리에서 다른 new 처리자를 설치한다.

 

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

set_new_handler 포인터를 넘긴다.

new처리자 미설치 시, operator new는 메모리 할당이 실패했을 때 예외를 던진다.

 

4. 예외를 던진다.

bad_alloc 혹은 bad _alloc에서 파생된 타입의 예외를 던진다.

operator new 쪽 종류의 에러를 받아서 처리하는 부분이 없기 때문에 예외는 메모리 할당을 요청한 원래의 위치로 전파된다.

 

5. 복귀하지 않는다.

abort, exit 호출하여 종료시킨다.

 

객체의 클래스 타입에 따라 메모리 할당 실패에 대한 처리를 다르게 하는 방법

아래와 같이 할당된 객체의 클래스 타입에 따라 메모리 할당 실패에 대한 처리를 다르게 가져가고 싶은 경우가 존재한다.

class X
{
public:
  static void outOfMemory();
  ...
};

class Y
{
public:
  static void outOfMemory();
  ...
};

X* p1 = new X; // 메모리 할당이 실패했을 경우 X::outOfMemory를 호출한다.
Y* p2 = new Y; // 메모리 할당이 실패했을 경우 Y::outOfMemory를 호출한다.

결국 아래처럼 클래스에서 자체 버전의 set_new_handler 및 operator new를 제공하도록 만들면 된다.

class NewHandlerHolder
{
public:
  // 현재의 new 처리자 획득
  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&); // 복사를 막기위한 부분
}

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::set_new_handler(std::new_handler p) throw()
{
  std::new_handler oldHandler = currentHandler;
  currentHandler = p;
  return oldHandler;
}

void * Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
  // Widget의 new 처리자 설치
  NewHandlerHolder h(std::set_new_handler(currentHandler)); 

  // 메모리를 할당하거나 할당이 실패하면 예외를 던짐
  return ::operator new(size);
}

 

신기하게 반복되는 템플릿 패턴(CRTP)

위의 코드는 특정 클래스에서 동작하지만, 자원관리 객체를 통한 할당 에러 처리를 구현하는 이런 방식의 코드는 어떤 클래스를 쓰더라도 똑같이 나올 것이다.

그래서 클래스 템플릿을 생성하고 이를 상속받아 사용하면 좋을 것 같다.

아래의 코드를 봐보자.

template<typename T>
class NewHandlerSupport // 클래스 별 set_new_handler를 지원하는 믹스인 양식의 기본 클래스
{
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:
  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)
{
  NewHandlerSupport h(std::set_new_handler(currentHandler));
  return ::operator new(size);
}

template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0; // 널로 초기화

class Widget: public NewHandlerSupport<Widget>
{
// 이전과 같지만 set_new_handler, operator new 선언문이 빠져있음
};

 

예외 불가 생성자

class Widget {...};

Widget *pw1 = new Widget; // 할당이 실패하면 bad_alloc예외

Widget *pw2 = new (std::nothorw) Widget; // 할당 실패 시 null 반환

위의 코드처럼 nothorw로 사용할 수 있으며 메모리 할당을 할 수 없을 때 널을 반환하게 하는 역할을 한다.

 

요약

  • set_new_handler 함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출되는 함수를 지정할 수 있다.
  • 예외 불가(nothorw) new는 영향력이 제한되어 있다. 메모리 할당 자체에만 적용되기 때문에 이후에 호출되는 생성자에서는 얼마든지 예외를 던질 수 있다.
728x90
반응형