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

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 8. new와 delete를 내 맘대로

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

✅ new 처리자란?

메모리 할당이 실패했을 때의 처리 방식:

  • operator new 함수는 할당할 수 있는 메모리가 없을 때 예외를 던진다.
    • 옛날에는 예외 대신 널 포인터를 반환했다.
  • 하지만 예외를 던지기 전에, 사용자 지정 에러 처리 함수를 우선적으로 호출한다.
    • 이를 new 처리자(new-handler, 할당에러 처리자) 라고 한다.

표준 라이브러리의 set_new_handler 함수:

namespace std {
    // new_handler는 함수 포인터 타입
    typedef void (*new_handler)();

    new_handler set_new_handler(new_handler p) throw();
}

set_new_handler 함수 분석:

  • 마지막의 throw()예외 지정이며, 어떤 예외도 던지지 않음을 의미한다.
  • 매개변수반환값 모두 new_handler 타입이다.
  • new_handleroperator new에서 예외가 발생했을 때 호출할 함수 포인터이다.
  • 반환값set_new_handler가 호출되기 전에 가지고 있던 new 처리자이다.

✅ new 처리자 사용 예시

// operator new에서 예외 발생 시 호출될 함수
void outOfMem() {
    cerr << "메모리 할당에 실패하였습니다.\n";
    abort();
}

int main() {
    // new 예외 발생 시 호출될 함수 지정
    set_new_handler(outOfMem);
    int* pBigDataArray = new int[100000000L];
    ...
}
  • 메모리 할당에 실패하면 outOfMem 함수가 호출된다.

주의사항:

  • 만약 cerr에 에러 메시지를 쓰는 도중 메모리가 동적 할당되어야 한다면, new 처리자를 반복 호출할 수 있다.

✅ new 처리자가 해야 할 다섯 가지 동작

new 처리자는 다음 중 하나를 수행해야 한다:

1. 사용할 수 있는 메모리를 더 많이 확보

operator new가 시도하는 이후의 메모리 확보를 성공하도록 하는 전략이다.

구현 방법:

  1. 프로그램이 시작할 때 메모리 블록을 크게 하나 할당한다.
  2. new 처리자가 처음 호출될 때, 그 메모리를 사용하도록 한다.


2. 다른 new 처리자 설치

현재의 new 처리자가 가용 메모리를 확보할 수 없더라도, 다른 new 처리자는 처리해줄 방법이 있을 수 있다.

void handler1() {
    cerr << "handler1: 메모리 부족! handler2로 교체합니다.\n";
    set_new_handler(handler2); // 다음 실패 시 handler2 호출
}

// new_handler가 재호출되었을 때도 여전히 할당할 수 없다면
// abort함으로써 해결하는 함수
void handler2() {
    cerr << "handler2: 여전히 부족! 프로그램 종료.\n";
    abort(); // 종료
}

int main() {
    set_new_handler(handler1); // 초기 handler 설정

    // 매우 큰 메모리 요청 → 실패 유도
    int* p = new int[1000000000]; // 실패 → handler1 → handler2 → 종료
}


3. new 처리자 설치 제거

  • set_new_handler에 널 포인터를 넘긴다.
  • 설치된 new 처리자가 없다면, operator new는 메모리 할당 실패 시 예외를 던진다.


4. 예외 던지기

  • bad_alloc 혹은 bad_alloc에서 파생된 타입의 예외를 던진다.
  • operator new는 이 종류의 에러를 처리하는 부분이 없다.
  • 그러므로 메모리 할당을 요청한 원래 위치로 예외를 전파(다시 던짐)한다.


5. 탈출

  • abort 혹은 exit를 호출하여 프로그램을 종료한다.
  • 위의 handler2 함수에서 채용한 방법이다.

✅ 클래스별 new 처리자 구현하기

목표: 할당된 객체의 클래스 타입에 따라 메모리 할당 실패 처리를 다르게 하기

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

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

int main() {
    // 메모리 할당 실패 시, X::outOfMemory 호출
    X* p1 = new X;
    // 메모리 할당 실패 시, Y::outOfMemory 호출
    Y* p2 = new Y;
}

문제:

  • C++은 특정 클래스만을 위한 할당에러 처리자를 두는 기능이 없다.
  • 하지만 직접 구현이 가능하다.

해결 방법:

  • 해당 클래스에서 자체 버전의 set_new_handleroperator new를 제공한다.
    • set_new_handler: 사용자로부터 new 처리자를 받아낸다.
    • operator new: 전역 new 처리자 대신 클래스 버전의 new 처리자가 호출되도록 한다.

✅ Widget 클래스의 클래스별 new 처리자 구현

Widget.h:

class Widget {
public:
    static new_handler set_new_handler(new_handler p) throw();
    static void* operator new(size_t size) throw(bad_alloc);

private:
    static new_handler currentHandler;
};

// Null로 초기화
new_handler Widget::currentHandler = 0;

Widget.cpp:

#include "Widget.h"
#include "NewHandlerHolder.h"

// 전역 set_new_handler와 동일한 행동을 수행한다
new_handler Widget::set_new_handler(new_handler p) throw() {
    new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

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

    // 메모리 할당에 성공하면: 기본 메모리 할당 함수를 호출한다
    return ::operator new(size);
    /* 메모리 할당에 실패하면:
    1. bad_alloc 예외를 던진다
    2. NewHandlerHolder의 소멸자가 호출된다
    3. 이전의 new 처리자로 복원된다
    */
}

NewHandlerHolder.h:

class NewHandlerHolder {
public:
    // 현재 가지고 있는 new 처리자를 획득한다
    explicit NewHandlerHolder(new_handler nh)
    : handler(nh) {}

    // 소멸하며, 생성자로 받았던 new 처리자(nh)로 복원된다
    ~NewHandlerHolder() { set_new_handler(handler); }

private:
    new_handler handler;

    // 복사 막기 (항목 14 참조)
    NewHandlerHolder(const NewHandlerHolder&);
    NewHandlerHolder& operator=(const NewHandlerHolder&);
};

사용 예시:

#include "Widget.h"

void outOfMem();

int main() {
    // Widget 객체에 대한 메모리 할당이 실패했을 때 호출될 함수 선언
    Widget::set_new_handler(outOfMem);

    // 메모리 할당 실패 시, outOfMem 호출
    Widget* pw1 = new Widget;
    // 메모리 할당 실패 시, 전역 new 처리자 함수(있다면) 호출
    string* ps = new string;

    // Widget의 new 처리자 함수를 null로 설정
    Widget::set_new_handler(0);
    // 메모리 할당 실패 시, 예외 던짐
    Widget* pw2 = new Widget;
}

✅ 믹스인(mixin) 양식을 통한 재사용

목적:

  • 자원 관리 객체를 통한 할당에러 처리를 구현하는 코드를 여러 클래스에서 공유하고 싶다.

방법:

  1. 다른 파생 클래스들이 한 가지의 특정 기능만을 물려받아 갈 수 있도록 설계된 기본 클래스를 만든다.
  2. 이후 만든 기본 클래스를 템플릿으로 만든다.

효과:

  • 기본 클래스 부분이 set_new_handler, operator new 함수를 물려준다.
  • 템플릿 부분은 파생 클래스가 인스턴스화되며 currentHandler 변수를 각자 가진다.

→ 파생 클래스마다 클래스 데이터의 사본이 각자 존재하여, 할당에러 처리 기능을 다른 클래스에서도 쓸 수 있다.

NewHandlerSupport.h:

template<typename T>
class NewHandlerSupport {
public:
    static new_handler set_new_handler(new_handler p) throw();
    static void* operator new(size_t size) throw(bad_alloc);
    ...

private:
    static new_handler currentHandler;
};

NewHandlerSupport.cpp:

template<typename T>
new_handler NewHandlerSupport<T>::set_new_handler(new_handler p) throw() {
    new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

template<typename T>
void* NewHandlerSupport<T>::operator new(size_t size) throw(bad_alloc) {
    NewHandlerHolder h(set_new_handler(currentHandler));
    return ::operator new(size);
}

template<typename T>
new_handler NewHandlerSupport<T>::currentHandler = 0;

Widget.h:

#include "NewHandlerSupport.h"

// 이젠 set_new_handler 혹은 operator new 선언이 빠짐
class Widget: public NewHandlerSupport<Widget> {
    ...
};

특징:

  • NewHandlerSupport 템플릿은 T를 직접 쓸 필요가 없다.
    • T는 파생 클래스들을 구분해주는 역할만 한다.
  • 템플릿 매개변수로 Widget을 가지는 기본 클래스로부터 Widget이 파생되었다.
    • 뭔가 이상해 보이지만 괜찮다.
    • 신기하게 반복되는 템플릿 패턴(Curiously Recurring Template Pattern: CRTP) 이라고 불린다.
  • 믹스인 양식을 쓰다 보면 다중 상속이 될 수 있다. (항목 40 참조)

✅ 예외불가(nothrow) new

역사:

  • 1993년까지는 operator new가 메모리 할당을 할 수 없을 때 널 포인터를 반환했다.
  • 현재는 bad_alloc을 던지지만, 옛날처럼 쓸 수도 있다.
  • 옛날 형태를 가리켜 “예외불가(nothrow)” 형태라고 한다.

사용 방법:

class Widget { ... };

// 현재의 방법
// 할당 실패 시, bad_alloc을 던진다
Widget* pw1 = new Widget;
if(pw1 == 0) ... // 이 점검 코드는 꼭 실패한다

// 옛날의 방법
// 할당 실패 시, 0(널)을 반환한다
Widget* pw2 = new (std::nothrow) Widget;
if(pw2 == 0) ... // 이 점검 코드는 성공할 수 있다

한계:

  • 예외불가 new는 호출되는 operator new에서만 예외가 발생되지 않도록 보장한다.
    • 즉, new (std::nothrow) Widget 표현식에서도 에러가 나올 수 있다.
    • 생성자에서 예외를 던질 수 있기 때문이다.
  • 따라서 예외불가 new는 실용성이 제한되어 있어 쓸 일이 거의 없다.

🧐 정리

  1. set_new_handler 함수를 쓰면, 메모리 할당 요청이
    만족되지 못했을 때 호출되는 함수를 지정할 수 있다.
  2. new 처리자는 아래 중 하나를 수행해야 한다.
    • 메모리 확보
    • 다른 처리자 설치
    • 처리자 제거
    • 예외 던지기
    • 프로그램 종료
  3. 클래스별로 자체 set_new_handleroperator new를 제공하면,
    클래스별 메모리 할당 실패 처리가 가능하다.
  4. 믹스인(mixin) 양식CRTP 패턴을 사용하면,
    할당에러 처리 코드를 여러 클래스에서 재사용할 수 있다.
  5. 예외불가(nothrow) new는 메모리 할당 자체에만 적용되기에 영향력이 제한되어 있다.
    이후 호출되는 생성자에서는 예외를 던질 수 있다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기