KoreanFoodie's Study

Effective C++ | 항목 49 : new 처리자의 동작 원리를 제대로 이해하자 본문

Tutorials/C++ : Advanced

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 를 다음과 같이 동작하게 한다.

  1. 표준 set_new_handler 함수에 Widget 의 new 처리자를 넘겨서 설치(전역 new 처리자로서 Widget 의 new 처리자를 설치) 한다.
  2. 전역 operator new 를 호출하여 실제 메모리 할당을 수행한다. 실패하면, 이 함수는 Widget 의 new 처리자를 호출한다(전 단계에서 처리자 설치했음). 마지막까지 전역 operator new 의 메모리 할당 시도가 실패하면, bad_alloc 예외를 던진다. 이 경우 Widget 의 operator new 는 전역 new 처리자를 원래의 것으로 되돌려 놓고 이 예외를 전파한다. 이때 전역 new 처리자를 자원으로 간주하고 처리한다.
  3. 전역 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 를 필요로 할 일은 거의 없을 것이다!

 

Comments