[C++] 항목 49: new 처리자의 동작 원리를 제대로 이해하자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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_handler는operator 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가 시도하는 이후의 메모리 확보를 성공하도록 하는 전략이다.
구현 방법:
- 프로그램이 시작할 때 메모리 블록을 크게 하나 할당한다.
- 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_handler및operator 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) 양식을 통한 재사용
목적:
- 자원 관리 객체를 통한 할당에러 처리를 구현하는 코드를 여러 클래스에서 공유하고 싶다.
방법:
- 다른 파생 클래스들이 한 가지의 특정 기능만을 물려받아 갈 수 있도록 설계된 기본 클래스를 만든다.
- 이후 만든 기본 클래스를 템플릿으로 만든다.
효과:
- 기본 클래스 부분이
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는 실용성이 제한되어 있어 쓸 일이 거의 없다.
🧐 정리
set_new_handler함수를 쓰면, 메모리 할당 요청이
만족되지 못했을 때 호출되는 함수를 지정할 수 있다.- new 처리자는 아래 중 하나를 수행해야 한다.
- 메모리 확보
- 다른 처리자 설치
- 처리자 제거
- 예외 던지기
- 프로그램 종료
- 클래스별로 자체
set_new_handler와operator new를 제공하면,
클래스별 메모리 할당 실패 처리가 가능하다. - 믹스인(mixin) 양식과 CRTP 패턴을 사용하면,
할당에러 처리 코드를 여러 클래스에서 재사용할 수 있다. - 예외불가(nothrow) new는 메모리 할당 자체에만 적용되기에 영향력이 제한되어 있다.
이후 호출되는 생성자에서는 예외를 던질 수 있다.
댓글남기기