C++/Effective C++

[Effective C++] 49: new 처리자의 동작 원리를 제대로 이해하자

KANTAM 2023. 11. 21. 03:34

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

메모리 할당이 제대로 되지 못한 상황에 대한 반응으로 operator new가 예외를 던지기 전에, 이 함수가 사용자 쪽에서 지정할 수 있는 에러 처리 함수를 우선적으로 호출하도록 되어 있는데, 이 에러 처리 함수를 가리켜 new 처리자(new-handler)라고 한다. 표준 라이브러리에는 set_new_handler라는 함수가있다. 

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

int main()
{
    std::set_new_handler(outOfMem);

    int* pBigDataArray = new int[100000000000];
}

매개변수는 요구된 메모리를 operator new가 할당하지 못했을 때 operator new가 호출할 함수의 포인터이다.

 

operator new는 충분한 메모리를 찾아낼 때까지 new 처리자를 되풀이해서 호출한다. new 처리자 함수가 프로그램 동작에 좋은 영향을 미치는 쪽으로 설계되어 있다면 다음 동작 중 하나를 꼭 해주어야 한다.

  • 사용할 수 있는 메모리를 더 많이 확보한다. 
    • operator new가 시도하는 이후의 메모리 확보가 성공할 수 있도록 하자는 전략
  • 다른 new 처리자를 설치한다.
    • 다른 new 처리자의 존재를 알고 있을 가능성이 있을 때
  • new 처리자의 설치를 제거한다.
    • set_new_handler에 널 포인터를 넘긴다.
  • 예외를 던진다.
    • bad_alloc 혹은 bad_alloc에서 파생된 타입의 예외를 던진다.
  • 복귀하지 않는다.
    • abort혹은 exit를 호출

 

클래스 타입에 따라서 메모리 할당 실패에 대한 처리를 다르게 가져가고 싶을 때는 어떻게 할까? 해당 클래스에서 자체 버전의 set_new_handler 및 operator new를 제공하도록 만들어 주면 된다.

operator new가 충분한 메모리를 할당하지 못할 경우에 호출될 new 처리자 함수를 어딘가에 간수해 둘 필요가 있으므로, 이 new 처리자를 가리키는 new_handler 타입의 정적 멤버 데이터를 선언한다. 

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;    // 널로 초기화, 클래스 구현 파일에 두어야 한다.

set_new_handler 함수는 자신에게 넘어온 포인터를 아무런 점검없이 저장해 놓고, 바로 전에 저장했던 포인터를 역시 아무런 점검 없이 반환하는 역할만 맡는다. 

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

 

이제 operator new가 할 일만 남았다. 

  1. 전역 new 처리자로서 Widget의 new 처리자를 설치한다. 
  2. 전역 operator new를 호출하여 실제 메모리 할당을 수행한다. 전역 operator new의 할당이 실패하면, 이 함수는 Widget의 new 처리자를 호출하게 된다. 마지막까지 전역 operator new의 메모리 할당 시도가 실패하면, 전역 operator new는 bad_alloc 예외를 던진다. 이 경우 Widget의 operator new는 전역 new 처리자를 원래의 것으로 되돌려 놓고, 이 예외를 전파시켜야 한다. 자원 관리 객체를 사용하여 전역 new 처리자를 관리함으로써 자원 누수를 막는다. 
  3. 전역 operator new가 Widget 객체 하나만큼의 메모리를 할당할 수 있으면, Widget 의 operator new는 이렇게 할당된 메모리를 반환한다. 이와 동시에, 전역 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;

    NewHandlerHolder(const NewHandlerHolder&);
    NewHandlerHolder& operator=(const NewHandlerHolder&);
};

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

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

// 이전의 전역 new 처리자가 자동으로 복원된다.

 

Widget 클래스를 사용하는 쪽에서  new 처리자 기능을 쓰려면 다음과 같이 하면된다. 

void outOfMem();

Widget::set_new_handler(outOfMem);

Widget* pw1 = new Widget;

std::string* ps = new std::string;

Widget::set_new_handler(0);

 

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

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:
    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;

이 템플릿의 매개변수인 T는 그냥 파생 클래스들을 구분해 주는 역할만 한다. 템플릿 메커니즘 자체는 NewHandlerSupport가 인스턴스화될 때 전달되는 T를 위한 currentHandler의 사본을 자동으로 찍어내는 공장인 셈이다. 이런 패턴을 신기하게 반복되는 템플릿 패턴(curiously recurring template pattern: CRTP)이라고 부른다. 책의 필자는 "나만의 것(Do It For Me)"라는 이름을 쓴다.

 

이것만은 잊지 말자!

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