본문 바로가기
책 정리/Effective C++ 3rd

항목 49. new 처리자의 동작원리를 제대로 이해하자

by ocean20 2020. 4. 1.

1. 메모리 할당 과정

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

하지만, 예외를 던지기전 사용자 error 처리함수를 호출하도록 설계되어있다.

표준라이브러리에서는 사용자 error 처리함수를 set 있는 함수를 준비해놓았다. => set_new_handler

namespace std{
  typedef void(*new_handler) ();  
  // 함수포인터(new_handler는 매개변수도 없고 반환형도 void인 함수포인터에 typedef를 걸어 놓았다.)
  
  new_handler set_new_handler(new_handler p) throw(); 
  // set_new_handler는 이러한 함수포인터(new_handler)를 받고 new_handler를 리턴하는 함수이다.
}

* 풀이

  1) 위 throw() 예외지정(exception specification)이라고 불리운다. 풀이하면 함수는 어떤 예외도 던지지 않을 거라는 뜻이다. (항목 29 참조)

  2) set_new_handler 함수의 매개변수는 메모리할당 실패시 operator_new 호출할 함수의 포인터(에러처리함수)이다.

반환값은 지금의 set_new_handler 호출되기 직전의 new 처리자로 쓰이던 함수포인터(이전 에러처리함수)이다.

void outOfMem ()  // 사용자 에러처리함수
{
  std::cerr << "Unable to satisfy request for memory\n";
  std::abort();
}

int main()
{
  std::set_new_handler(outOfMem);
  int *pBigDataArray = new int[10000000L];
  …
}

*풀이 : 만약 operator_new 1억개의 정수 할당의 실패하면 outOfMem(사용자 에러처리함수, new_handler) 호출하고, 강제종료할것이다.

 

 

2. new 처리자의 올바른 설계

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

    - 프로그램이 시작할때, 메모리 블록을 크게 할당해 놓았다가 new처리자 호출(메모리할당 첫실패시) 메모리를 쓸수있도록한다.

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

    - 호출된 현재의 new처리자가 감당하지 못할때, 다른 new처리자를 설치한다.(현재의 new 처리자에서 set_new_handler(다른 new_handler) 호출)

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

    - set_new_handler 포인터를 넘긴다. new처리자 미설치시, operator new 처음얘기한것처럼 사용자에러처리함수 없을시, 예외를 던진다.

  4) 예외를 던진다.

    - bad_alloc 혹은 bad _alloc에서 파생된 타입의 예외를 던진다. operator_new 쪽종류의 에러 대응 능력이 없기에, 예외는 메모리 할당을 호출한쪽으로 예외 전이(propagate)된다.

  5) 복귀하지 않는다.

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

 

3. 할당된 객체의 클래스 타입에 따라 다른 메모리 할당 실패 처리를 다르게 가져가고 싶을때

- 해당클래스에서 자체 버전의 set_new_handler 그리고 operator_new 제공하도록 만들어주면된다.

std::new_handlerWidget::currentHandler = 0;

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 current Handler; // set_new_handler 호출시, 기존 에러처리함수포인터를 담는다.
};

std::new_handler Widget::set_newHandler(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 메모리를 할당한다. 이때 에러발생시, 1번에서 설치한 Widget new 처리자가 호출된다. 만약 이것마저도 실패시 예외를 발생시켜, new 처리자를 old new처리자로 교체한다. 그리고 예외를 전파한다. 항목13 조언대로 자원관리 기법을 사용하여, 전역 new 처리자를 관리한다.

  3) 객체 소멸시 전역 new처리자를 자동 복원하게 설계한다.

 

코드를 보자.

class NewHandlerHolder {  // 자원관리 객체
public :
  explicit NewHandlerHolder( std::new_handler nh) // new 처리자  획득
  : 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)
{
  NewHandlerHolder h(std::set_new_handler(currentHandler));  
  // Widget의 new처리자를 할당하고 기존 new처리자를 h에 넘긴다.

  return ::operator new(size); // 메모리를 할당한다. 실패시에는 예외를 던진다. 이전의 전역 new처리자가 자동으로 복원된다. 왜? 소멸자가 핸들러 복귀시키므로..
}

 

 

void outOfMem();  // Widget객체 메모리 할당실패시 호출할 함수 선언

Widget::set_new_handler(outOfMem); // new 처리자 설치(outOfMem)

Widget *pw1 = new Widget;  // 할당시도, 실패시 outOfMem 호출

std::string *ps = new std::string; // 전역 new 처리자 호출

Widget::set_new_handler(0);  // new 처리자 미설치 상태

Widget *pw2 = new Widget;  // operator new는 사용자 에러처리함수가 없으므로, 예외 던진다.

 

실제 책에는 템플릿을 이용한 "믹스인 양식" "신기하게 반복되는 템플릿 패턴 : CRTP" 내용을 다루고 있으나, 여기서는 생략하겠다. (이정도만 알아도 충분할듯하다. 2회차때 읽어보도록 해야겠다.


이것만은 잊지 말자!

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

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

댓글