일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- virtual function
- 게임
- 비교 함수 객체
- exception
- 영화
- Smart Pointer
- 예외
- 상속
- 스마트 포인터
- 암시적 변환
- resource management class
- Vector
- 참조자
- 티스토리챌린지
- 다형성
- 루아
- c++
- 메타테이블
- reference
- 반복자
- implicit conversion
- lua
- 영화 리뷰
- 언리얼
- Effective c++
- UE4
- effective stl
- operator new
- 오블완
- more effective c++
Archives
- Today
- Total
스토리텔링 개발자
[Effective C++] 49. new 처리자 본문
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 처리자가 해야 하는 동작
(이 동작 중 하나는 꼭 해야 좋은 설계이다.)
- 사용할 수 있는 메모리를 더 많이 확보한다.
- operator new가 시도하는 이후의 메모리 확보가 성공할 수 있도록 하자는 전략이다.
- 예를 들면 프로그램 시작 시 메모리 블록을 하나 크게 할당해두고, new 처리자가 호출될 때 해당 메모리를 쓸 수 있게 해준다거나.
- 다른 new 처리자를 설치한다.
- 현재 new 처리자가 더 이상 가용 메모리를 확보하지 못한다 해도, 다른 new 처리자가 그걸 해줄 수 있다는 사실을 알고 있을 수 있다.
- 제자리에서 다른 new 처리자를 설치해버리면 된다.
-
void outOfMem() { std::set_new_handler(outOfMem2); // 다른 new 처리자 설치 } int main() { std::set_new_handler(outOfMem); ... }
- 이 방법을 비틀어서...
- new 처리자가 자기 자신의 동작을 변경하도록 구현할 수도 있을 것이다.
- new 처리자의 설치를 제거한다.
- 즉 set_new_handler에 널 포인터를 넘긴다.
- new 처리자가 설치된 것이 없으면, operator new는 메모리 할당에 실패했을 때 예외를 던지게 된다.
- 예외를 던진다
- bad_alloc나 bad_alloc에서 파생된 예외를 던진다.
- operator new에는 bad_alloc 관련 에러를 받아서 처리하는 부분이 없으므로
- 메모리 할당을 요청한 원래 위치로 예외를 전파하게 된다.
- 복귀하지 않는다.
- 대개 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 처리자를 기억해두는 정적 멤버 데이터)의 사본이 따로 따로 존재한다.
- 효과
- 기본 클래스 부분은 파생 클래스들이 가져야 하는 함수를 물려준다.
- set_new_handler 함수
- operator new 함수
- 템플릿 부분이 각 파생 클래스에서 인스턴스화된 클래스가 되면서 정적 데이터 멤버를 따로따로 가질 수 있게 된다.
- 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 표현식의 두 가지 동작
- operator new 함수의 예외불가 버전이 호출되어 Widget 객체를 담기 위한 메모리 할당을 시도한다.
- 만약 할당이 실패하면?
- operator new는 널 포인터를 반환한다.
- 만약 할당이 성공하면?
- Widget 생성자가 호출된다.
- 하지만 Widget 생성자는 지 멋대로 할 수 있다.(즉 예외불가 new로부터 전혀 제약을 받지 않는다.)
- 즉 생성자에서 예외가 빠져나올 수 있을 것이다.
- 그러니 결과적으론 예외불가 new가 필요할 일은 없을 것이다.
- 하지만 두 버전(일반 new / 예외불가 new) 모두 new 처리자는 쓰인다.
728x90
'개발 > Effective C++' 카테고리의 다른 글
[Effective C++] 51. operator new / delete 커스텀 관례 (0) | 2024.07.25 |
---|---|
[Effective C++] 50. operator new / delete는 언제 커스텀해야 할까? (2) | 2024.07.24 |
[Effective C++] 48. 템플릿 메타 프로그래밍 (0) | 2024.07.22 |
[Effective C++] 47. 특성 정보 클래스 (0) | 2024.07.19 |
[Effective C++] 46. 템플릿 클래스 안에 비멤버 함수 두기 (0) | 2024.07.18 |
Comments