[Effective C++] Chapter 8. new와 delete를 내 맘대로
Table of Contents
이펙티브 C++ 책을 읽고 공부한 노트입니다.
Item 49: new
처리자의 동작 원리를 제대로 이해하자 #
new
처리자(new
handler) #
new
로 메모리 할당이 제대로 되지 못하면 호출되는 에러 처리 함수이다.- 사용자가 직접 지정할 수 있으며, 표준 라이브러리에 있는
set_new_handler
함수를 이용하면 된다.
// <new>에 선언되어 있는 함수들
namespace std
{
typedef void (*new_handler)();
// 매개변수와 반환값이 없는 함수에 대한 포인터이다.
new_handler set_new_handler(new_handler p) throw();
// 매개변수 : 새롭게 설치할 new 처리자
// 반환값 : 새롭게 설치하기 전까지 사용하던 new 처리자
// throw() : 이 함수는 어떤 예외도 던지지 않을 것이라는 뜻이다.
}
new
처리자는 이것들 중 하나는 꼭 처리해야 한다.- 사용할 수 있는 메모리를 더 많이 확보한다.
- 다른
new
처리자를 설치한다. new
처리자의 설치를 제거한다.bad_alloc
혹은 그것에서 파생된 타입의 예외를 던진다.- 복귀하지 않는다. (대개
abort
혹은exit
을 호출한다.)
new
처리자를 클래스 타입에 따라 다르게 만들기 #
// 자원 관리 객체를 통해 할당에러를 처리한다.
class NewHandlerHolder
{
private:
std::new_handler handler;
// 복사를 막기 위함 (Item 14)
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
public:
// 매개변수로 들어온 처리자를 들고 있다가
explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {}
// 객체가 소멸될 때 그 처리자를 설치한다.
~NewHandlerHolder() { std::set_new_handler(handler); }
};
class Widget
{
private:
static std::new_handler currentHandler;
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);
};
std::new_handler Widget::currentHandler = 0; // 널로 초기화
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)
{
// 가지고 있는 현재 처리자를 설치하고,
// 이전 처리자는 NewHandlerHolder가 가지고 있다가
// 이 함수를 빠져나가면 이전 처리자(전역 처리자)로 재설치한다.
NewHandlerHolder h(std::set_new_handler(currentHandler));
// 메모리를 할당하고, 실패하면 예외를 던진다.
return ::operator new(size);
}
void outOfMem();
int main()
{
// 새로운 처리자 설치
Widget::set_new_handler(outOfMem);
// 여기서 new 실패시 outOfMem이 불린다.
Widget * pw1 = new Widget;
// 여기서 new 실패시 전역 new 처리자가 불린다.
std::string * ps = new std::string;
// null로 설정
Widget::set_new_handler(0);
// 여기서 new 실패시 예외를 바로 던진다.
Widget * pw2 = new Widget;
}
NewHandlerHolder
를 템플릿으로 바꿔서 다른 클래스에서도 재사용 할 수 있게 해보자.
template<typename T>
class NewHandlerSupport
{
private:
static std::new_handler currentHandler;
public:
static std::new_handler set_new_handler(std::new_hanlder p) throw();
static void * operator new(std::size_t size) throw(std::bad_alloc);
};
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_hanlder p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void * newHandlerSupport<T>::operator new(std::size_t size) throw(bad_alloc)
{
NewHandlerSupport h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
// 신기하게 반복되는 템플릿 패턴(curiously recurring template pattern: CRTP)
// Widget이 필요한건, NewHandlerSupport에 대한 사본이다.
// NewHandlerSupport은 마치, 인스턴스화될 때 전달되는 T를 위한
// NewHandlerSupport의 사본을 찍어내는 공장과 같다.
class Widget : public NewHandlerSupport<Widget>
{
// ...
};
예외불가(nothrow) new
#
- 현재는
new
실패 시bad_alloc
예외를 던지도록 되어 있다. - 예외불가
new
는 예전의 방식대로, 실패 시null
을 반환하는 것이다.
Widget * pw1 = new Widget;
// Widget을 할당하다가 실패하면 bad_alloc 예외를 던진다.
if (pw1 == 0)
// 이것은 늘 false일 것이다.
Widget * pw2 = new (std::nothrow) Widget;
// Widget을 할당하다가 실패하면 0(널)을 반환한다.
if (pw2 == 0)
// 실패하면 널을 반환하므로 점검해보는 것이 가능하다.
- 참고로 예외불가
new
는operator new
에서만 예외가 발생하지 않도록 보장하는 것일 뿐,Widget
의 생성자에서 예외가 나오지 않게 막아 준다는 것은 아니다.
Item 50: new
및 delete
를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자 #
- 사용자 정의
new
와delete
를 작성해야 하는 때란?- 잘못된 힙 사용을 탐지하기 위해
- 효율을 향상시키기 위해
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 할당 및 해제 속력을 높이기 위해
- 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
- 적당히 타협한 기본 할당자의 바이트 정렬 동작을 보장하기 위해
- 임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해
- 그때그때 원하는 동작을 수행하도록 하기 위해
- 바이트 정렬(byte alignment) 문제
- 컴퓨터는 아키텍처 적으로 특정 타입의 데이터가 특정 종류의 메모리 주소를 시작 주소로 하여 저장될 것을 요구사항으로 두고 있다.
- 예를 들면, 포인터는 4바이트 단위로 정렬되어야 한다거나
double
은 8바이트 단위로 정렬되어야 한다는 것이다. - 모든
operator new
함수는 어떤 데이터 타입에도 바이트 정렬을 적절히 만족하는 포인터를 반환해야 한다. - 이런 바이트 정렬을 어떻게 다루느냐에 따라 사용자 정의 버전을 제공하는 메모리 관리자의 품질이 달라질 것이다.
Item 51: new
및 delete
를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 #
operator new
를 구현할 때 지켜야할 요구사항들 #
- (1) 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 한다.
- (2) 할당 실패 시
new
처리자 함수를 호출해야 한다. - (3) 크기가 없는(0바이트) 메모리 요청에 대한 대비책을 마련해야 한다.
- (4) 기본 형태의
new
가 가려지지 않도록 해야한다.
void * operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
// (3) 0바이트 메모리 요청이 들어오면 1바이트 요구로 간주한다.
if (size == 0) size = 1;
// (1) opreator new 함수에는 무한 루프가 있다.
// 그래서 메모리 할당에 성공하든지,
// 아니면 Item 49에서 나왔던 것들 중 하나는 꼭 처리해야 한다.
while(true)
{
size바이트를 할당해 본다.
if (할당 성공) return 할당한 메모리의 포인터;
// 현재 설정되어 있는 new 처리자를 얻는다.
new_handler globalHandler = set_new_handler(0);
set_new_hanlder(globalHandler);
// (2) new 처리자를 호출한다.
if (globalHanlder) (*globalHandler)();
else throw std::bad_alloc();
}
}
- 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰(다른) 메모리 블록에 대한 요구도 처리해야 한다.
- 클래스
X
를 위한operator new
는sizeof(X)
인 크기의 객체에 맞추어져 있다. - 그래서
Base
클래스에서 선언된operator new
을Derived
가 호출하면 매개변수로 넘어오는size
가 달라지게 된다. (Base
와Derived
의 크기는 다르다. ) - 따라서 다음과 같은 처리가 필요할 것이다.
- 클래스
void * Base::operator new(std::size_t size) throw(bad_alloc)
{
if (size != sizeof(Base))
return ::operator new(size);
// size가 Base와 다르면 표준 operator new 쪽에서 처리하도록 넘긴다.
}
operator delete
를 구현할 때 지켜야할 요구사항들 #
- 널 포인터에 대한
delete
적용이 항상 안전하도록 보장해야 한다. - 클래스 전용 버전의 경우에는
new
와 똑같이 크기를 점검하는 코드를 넣어준다.
void Base::operator delete(void * rawMemory, std::size_t size) throw()
{
// 널 포인터의 경우 아무것도 하지 않는다.
if (rawMemory == 0) return;
// 클래스 전용 버전인 경우 size를 점검한다.
if (size != sizeof(Base))
{
::operator delete(rawMemory); // 표준 operator delete가 처리하도록 한다.
return;
}
// ... rawMemory가 가리키는 메모리를 해제한다.
}
- 추가적으로, 가상 소멸자가 없는 기본 클래스로부터 파생된 클래스의 객체를 삭제하려고 할 경우에는, C++이
operator delete
로 넘기는size_t
값이 엉터리일 수 있다.- 따라서 기본 클래스는 반드시 가상 소멸자를 두어야 하겠다.
Item 52: 위치지정 new
를 작성한다면 위치지정 delete
도 같이 준비하자 #
C++가 전역 유효범위에서 제공하는 operator new
의 세 가지 표준 형태 #
// 기본형 new
void * operator new(std::size_t) throw(std::bad_alloc);
// 위치지정 new
void * operator new(std::size_t, void *) throw();
// 예외불가 new (Item 49)
void * operator new(std::size_t, const std::nothrow_t&) throw();
위치지정(placement) new
#
- 추가적인 매개변수를 받는
operator new
이다. - 이름이 위치지정인 이유는 원조격인 위치지정
new
가 추가적인 매개변수로, 할당할 메모리의 위치를 받아서 할당 위치를 정해주었기 때문이다.
- 런타임 시스템은
operator new
가 받아들이는 매개변수의 개수 및 타입이 똑같은 버전의operator delete
를 찾고, 그 녀석을 호출한다.- 따라서 위치지정
new
를 만들었다면, 그와 똑같은 매개변수를 받아들이는 위치지정delete
를 만들어야 한다. - 위치지정
delete
가 호출되는 때는, 위치지정new
와 함께 호출된 생성자에서 예외가 발생했을 때뿐이다.
- 따라서 위치지정
class Widget
{
public:
// 새로운 매개변수 ostream을 받는 operator new (비표준 형태)
static void * operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
// 클래스 전용 operator delete (표준 형태)
static void operator delete(void * rawMemory, std::size_t size) throw();
};
int main()
{
Widget * pw = new (std::cerr) Widget;
// 여기서 Widget생성자에서 오류가 난다면?
// 그럼 delete가 제대로 되겠는가?
// 새로운 매개변수 ostream을 받는 new와 짝이 맞는 delete가 없어서
// 결국 아무것도 불리지 않고 메모리는 누수가 된다.
// 따라서 새로운 매개변수 ostream을 받는 delete도 있어야 하겠다.
}
- 함수는 이름만 같아도 가려지게 되어있다(Item 33). 따라서 새로 정의한 위치지정
new
와delete
때문에 표준버전들이 가리지 않도록 주의해야한다.- 좋은 방법은, 기본 클래스 하나를 만들고, 이 안에
new
와delete
의 기본 형태를 전부 넣어두는 것이다. - 그 다음 상속과
using
선언을 사용해서 표준 형태를 파생 클래스로 끌어오고, 원하는 사용자 정의 형태를 선언하는 것이다.
- 좋은 방법은, 기본 클래스 하나를 만들고, 이 안에
class StandardNewDeleteForms
{
public:
// 기본형
static void * operator new(std::size_t size) throw(std::bad_alloc) { return ::operator new(size); }
static void operator delete(void * pMemory) throw() { ::operator delete(pMemory); }
// 위치지정
static void * operator new(std::size_t size, void *ptr) throw() { return ::operator new(size, ptr); }
static void operator delete(void * pMemory, void * ptr) throw() { ::operator delete(pMemory, ptr); }
// 예외불가
static void * operator new(std::size_t size, const std::nothrow_t& nt) throw() { return ::operator new(size, nt); }
static void operator delete(void * pMemory, const std::nothrow_t&) throw() { ::operator delete(pMemory); }
};
class Widget : public StandardNewDeleteForms
{
public:
// StandardNewDeleteForms의 표준 형태가 Widget 내부에서 보이도록 만든다.
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
// 새롭게 정의한 위치지정 new와 그와 짝이 맞는 위치지정 delete
static void * operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void *pMemory, std::ostream& logStream) throw();
};