스토리텔링 개발자

[Effective C++] 49. new 처리자 본문

개발/Effective C++

[Effective C++] 49. new 처리자

김디트 2024. 7. 23. 16:27
728x90

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

 

 

 

사용자의 메모리 할당 요청 실패 상황
  • operator new 함수는 예외를 던지게 되어 있다.
    • 오랜 옛날(구닥다리 컴파일러)에는 null을 반환했지만..
  • 그리고 예외를 던지기 전에, 사용자 쪽에서 지정할 수 있는 에러 처리 함수를 우선적으로 호출하게 되어 있다.

 

 

 

new 처리자(new-handler, 할당에러 처리자)
  • 사용자 쪽에서 지정할 수 있는 에러 처리 함수.
  • 표준 라이브러리에는 set_new_handler라는 함수가 준비되어 있다.
namespace std
{
    typedef void (*new_handler)();
    
    // 이전 new 처리자를 리턴한다.
    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);
    
    int* pBigDataArray = new int[100000000L];
    ...
}
  • operator new가 1억 개의 정수 할당에 실패하면 outOfMem 함수가 호출될 것이다.
    • 근데 cerr로 에러 메시지를 쓰는 과정에서 또 메모리가 동적으로 할당되어야 한다면 어떻게 될까??
    • 사용자가 부탁한 만큼의 메모리를 할당해 주지 못하면 operator new는 충분한 메모리를 찾아낼 때까지 new 처리자를 되풀이해서 호출하게 된다.

 

 

 

new 처리자가 해야 하는 동작
(이 동작 중 하나는 꼭 해야 좋은 설계이다.)
  1. 사용할 수 있는 메모리를 더 많이 확보한다.
    • operator new가 시도하는 이후의 메모리 확보가 성공할 수 있도록 하자는 전략이다.
    • 예를 들면 프로그램 시작 시 메모리 블록을 하나 크게 할당해두고, new 처리자가 호출될 때 해당 메모리를 쓸 수 있게 해준다거나.
  2. 다른 new 처리자를 설치한다.
    • 현재 new 처리자가 더 이상 가용 메모리를 확보하지 못한다 해도, 다른 new 처리자가 그걸 해줄 수 있다는 사실을 알고 있을 수 있다.
    • 제자리에서 다른 new 처리자를 설치해버리면 된다.
    • void outOfMem()
      {
          std::set_new_handler(outOfMem2); // 다른 new 처리자 설치
      }
      
      int main()
      {
          std::set_new_handler(outOfMem);
          ...
      }
    • 이 방법을 비틀어서...
      • new 처리자가 자기 자신의 동작을 변경하도록 구현할 수도 있을 것이다.
  3. new 처리자의 설치를 제거한다.
    • 즉 set_new_handler에 널 포인터를 넘긴다.
    • new 처리자가 설치된 것이 없으면, operator new는 메모리 할당에 실패했을 때 예외를 던지게 된다.
  4. 예외를 던진다
    • bad_alloc나 bad_alloc에서 파생된 예외를 던진다.
    • operator new에는 bad_alloc 관련 에러를 받아서 처리하는 부분이 없으므로
      • 메모리 할당을 요청한 원래 위치로 예외를 전파하게 된다.
  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를 호출하고 싶다.
  • C++에는 이를 위한 기능이 준비되어 있지 않다.
  • 하지만 직접 구현하면 된다.
    • 해당 클래스에서 자체 버전의 set_new_handler 및 operator new를 제공하도록 만들어주면 된다.
    • RAII (항목 13 참조)
// .h
// 자체적인 new 연산자를 가지는 클래스
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:
    std::new_handler currentHandler;
};

// new 설치자 관리를 위한 RAII 연산(항목 13 참조)
class NewHandlerHodler
{
public:
    // 현재의 전역 new 처리자를 획득한다.
    explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {}
    
    ~NewHandlerHolder()
    {
        // 해제 시 원복시킨다.
        std::set_new_handler(handler);
    }
private:
    std::new_handler handler;
    
    NewHandlerHolder(const NewHandlerHolder&); // 복사를 막는다.
    NewHandlerHodler& operator=(const NewHandlerHolder&); // 복사를 막는다.
};


// .cpp
std::new_handler Widget::currentHandler = 0; // 널로 초기화

// Widget 클래스가 지원하는 new 처리자 할당 함수
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);
{
    // 자원 관리 객체를 통한 set_new_handler 동작
    NewHandlerHodler h(std::set_new_handler(currentHandler));
    
    // 메모리를 할당에 실패하면 예외를 던진다.
    // 이 경우 NewHandlerHolder를 통해 이전의 전역 new 처리자가 자동으로 복원된다.
    return ::operator new(size);
}

void outOfMem();

Widget::set_new_handler(outOfMem); // 설정

// 할당 실패 시 outOfMem이 호출된다.
Widget* pw1 = new Widget;
// 할당 실패 시 전역 new 처리자 함수가(있으면) 호출된다.
std::string* ps = new std::string;

Widget::set_new_handler(0); // 설정 취소

Widget* pw2 = new Widget; // 이제 그냥 예외를 바로 던진다.

 

 

 

new 처리자 할당 코드를 재사용 할 수 있게 해보자
  • ‘믹스인(mixin) 양식’ 기본 클래스
    • 파생 클래스들이 한 가지의 특정 기능만을 물려받아갈 수 있도록 설계된 기본 클래스.
    • 위의 경우 '특정 기능'이란 클래스별 new 처리자를 설정하는 기능일 것이다.
    • 그렇게 만든 기본 클래스를 템플릿으로 바꾼다.
      • 이렇게 하면 파생 클래스마다 클래스 데이터(원래의 new 처리자를 기억해두는 정적 멤버 데이터)의 사본이 따로 따로 존재한다.
  • 효과
    1. 기본 클래스 부분은 파생 클래스들이 가져야 하는 함수를 물려준다.
      • set_new_handler 함수
      • operator new 함수
    2. 템플릿 부분이 각 파생 클래스에서 인스턴스화된 클래스가 되면서 정적 데이터 멤버를 따로따로 가질 수 있게 된다.
      • currentHandler 데이터 멤버
// .h
// 템플릿 기본 클래스
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);
    ...
private:
    static std::new_handler currentHandler;
}

// 기본 클래스를 상속받는다.
class Widget : public NewHandlerSupport<Widget>
{ ... }

// .cpp
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);
}

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

 

 

 

주의 깊게 볼 만한 포인트들
  • 헌데 NewHandlerSupport 템플릿 클래스에서는 타입 매개변수 T를 아예 사용하지 않는다.
    • 그냥 파생 클래스들을 구분해주는 역할이다.
    • NewHandlerSupport가 인스턴스화 될 때 전달되는 T 클래스를 위한 정적 데이터 멤버의 사본을 자동으로 찍어내기 위한 식별자일 뿐.
  • 템플릿 매개변수로 Widget을 받아 만들어진 기본 클래스를 상속받아 Widget이 만들어진다?
    • 신기하게 반복되는 템플릿 패턴(curiously recurring template pattern : CRTP)
    • 넌 나만의 템플릿이야. 라는 의미.
  • 믹스인 클래스를 상속받으면 어쩔 수 없이 다중 상속을 고려해야 한다. (항목 40 참조)

 

 

 

예외불가 new
  • 1993년까지는 C++은 oeprator new가 메모리 할당 실패 시 널포인터를 반환하도록 되어 있었다.
  • 그리고 몇년 후 bad_alloc 예외를 던지도록 명세가 바뀌었다.
  • 하지만 C++ 표준화 위원회는 '널 포인트 점검' 기반의 레거시 코드를 지원해야 했기에...
    • 전통적인 '할당 실패 시 널 반환'에 동작하는 대안적 형태의 operator new도 같이 내놓았다.
    • 이를 예외불가(nothrow) 형태라고 한다.
class Widget { ... };

// 일반적인 경우
Widget* pw1 = new Widget;
// 할당 실패 시 위에서 bad_alloc 예외가 던져지므로 아래 코드는 반드시 실패한다.
if(pw1 == 0) ...

// 예외불가 형태를 사용하는 경우
Widget* pw2 = new (std::nothrow) Widget;
// 아래 점검 코드는 성공할 수 있다.
if(pw2 == 0) ...
  • new (std::nothrow) Widget 표현식의 두 가지 동작
    1. operator new 함수의 예외불가 버전이 호출되어 Widget 객체를 담기 위한 메모리 할당을 시도한다.
    2. 만약 할당이 실패하면?
      • operator new는 널 포인터를 반환한다.
    3. 만약 할당이 성공하면?
      • Widget 생성자가 호출된다.
      • 하지만 Widget 생성자는 지 멋대로 할 수 있다.(즉 예외불가 new로부터 전혀 제약을 받지 않는다.)
      • 즉 생성자에서 예외가 빠져나올 수 있을 것이다.
  • 그러니 결과적으론 예외불가 new가 필요할 일은 없을 것이다.
  • 하지만 두 버전(일반 new / 예외불가 new) 모두 new 처리자는 쓰인다.
728x90
Comments