[Effective C++ 3판] Chapter 8. new와 delete를 내 맘대로 (항목 49 ~ 52)
Chapter 8. new와 delete를 내 맘대로 (항목 49 ~ 52)
항목 49. new 처리자의 동작 원리를 제대로 이해하자.
[new와 delete 키워드]new/new[] 키워드 : (알맞는 operator new 호출 -> 생성자 호출) + 알파 (operator new와 생성자를 적절하게 호출하고 메타데이터등을 저장하고 관리하는 코드. 여기서 메타데이터는 new[]할때 객체의 갯수등을 의미.)
delete/delete[] 키워드 : 위와 비슷한 맥락.
[new 처리자 - new handler]
operator new의 역할 : 필요한 메모리를 적절하게 할당해서 반환해주는 역할.
new 처리자 : operator new가 메모리를 할당하는데 실패했을 때 호출되는 함수.
[new 처리자가 해야하는 동작 (아래중 하나를 해야한다.)]
- 사용할 수 있는 메모리를 더 많이 확보한다.
- 다른 new 처리자를 설치한다.
- new 처리자의 설치를 제거한다.
- 예외를 던진다.
- 복귀하지 않는다. (abort나 exit을 호출)
#include <new>
// RAII방식으로 new handler을 관리하는 클래스
class NewHandlerRAII {
public:
explicit NewHandlerRAII(std::new_handler nh) noexcept {
old = std::set_new_handler(nh);
}
~NewHandlerRAII() noexcept {
std::set_new_handler(old);
}
NewHandlerRAII(const NewHandlerRAII&) = delete;
NewHandlerRAII& operator=(const NewHandlerRAII&) = delete;
private:
std::new_handler old;
};
// class별로 new handler를 지정할 수 있는 기능과 인터페이스를 담고 있는 클래스 템플릿.
template <class T>
class NewHandlerFeature {
public:
static std::new_handler set_new_handler(std::new_handler nh) noexcept {
auto old = currentHandler;
currentHandler = nh;
return old;
}
static void* operator new(std::size_t size) throw(std::bad_alloc) {
NewHandlerRAII nhr(currentHandler);
return ::operator new(size);
}
static void* operator new(std::size_t size, void *pMem) noexcept {
// NewHandlerRAII nhr(currentHandler);
return ::operator new(size, pMem);
}
static void* operator new(std::size_t size, const std::nothrow_t& nt) noexcept {
NewHandlerRAII nhr(currentHandler);
return ::operator new(size, nt);
}
// operator new[]는 생략..
private:
static std::new_handler currentHandler;
};
template <class T>
std::new_handler NewHandlerFeature<T>::currentHandler = nullptr;
// 신기하게 반복되는 템플릿 패턴 (CRTP : Curiously Recurring Template Pattern)
// also called to "나만의 것(Do It For Me)" by Scott Meyers.
// CRTP패턴을 사용함으로써 기본 클래스의 정적변수를 파생 클래스별로 각자 소유할 수 있게된다.
class Example : public NewHandlerFeature<Example> {
public:
Example() {}
~Example() {}
private:
char dummy[8];
};
void func() {}
int main() {
Example::set_new_handler(func);
auto ptr1 = new Example();
char buf[sizeof(Example)];
auto ptr2 = new (buf) Example();
auto ptr3 = new (std::nothrow) Example();
}
[이것만은 잊지 말자]
- set_new_handler 함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출되는 함수를 지정할 수 있습니다.
- 예외불가(nothrow) new는 영향력이 제한되어 있습니다. 메모리 할당 자체에만 적용되기 때문입니다. 이후에 호출되는 생성자에서는 얼마든지 예외를 던질 수 있습니다.
항목 50. new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자.
일단은 기본 제공 new/delete를 사용하고, 추후 프로그램 프로파일링등을 통해 얻은 정보를 바탕으로 new/delete를 적절히 교체한다. 직접 구현하려면 신경쓸 부분이 많으므로 우선적으로 오픈소스/상용 메모리 관리 함수를 찾아본다.[new/delete를 바꿔야 할 때]
- 잘못된 힙 사용을 탐지하기 위해
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 할당 및 해제 속력을 높이기 위해
- 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
- 적당히 타협한 기본 할당자의 바이트 정렬 동작을 보정하기 위해
- 임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해 (locality를 극대화하고 싶을 때..별로 힙생성등을 통해 관리)
- 그때그때 원하는 동작을 수행하도록 하기 위해 (메모리 할당/해제를 공유메모리로 부터 하고싶을 때 등등)
[이것만은 잊지 말자]
- 개발자가 스스로 사용자 정의 new 및 delete를 작성하는 데는 여러 가지 나름대로 타당한 이유가 있습니다. 여기에는 수행 성능을 향상시키려는 목적, 힙 사용 시의 에러를 디버깅하려는 목적, 힙 사용 정보를 수집하려는 목적 등이 포함됩니다.
항목 51. new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자.
#include <new>
class Example {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc) {
if (size == 0) { // 0 byte request.
size = 1;
}
else if (size != sizeof(Example)) { // new request of Derived Object.
return ::operator new(size);
}
void *pMem = malloc(size);
while (pMem == nullptr) {
std::new_handler nh = std::set_new_handler(nullptr);
std::set_new_handler(nh);
if (nh) {
nh();
}
else {
throw std::bad_alloc();
}
pMem = malloc(size);
}
return pMem;
}
static void operator delete(void *pMem) noexcept {
if (pMem == nullptr) { // NULL pointer.
return;
}
free(pMem);
}
};
int main() {
auto ptr = new Example();
delete ptr;
}
[이것만은 잊지 말자]
- 관례적으로, operator new함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0바이트에 대한 대책도 있어야 합니다. 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰(다른) 메모리 블록에 대한 요구도 처리해야 합니다.
- operator delete 함수는 널 포인터가 들어왔을 때 아무 일도 하지 않아야 합니다. 클래스 전용 버전의 경우에는 예정 크기보다 더 큰 블록을 처리해야 합니다.
항목 52. 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자.
// 항목33처럼 이름 가려짐 문제가 발생할 수 있다.
// 기본클래스 내부의 이름이 가려지는 건 using으로 해결할 수 있지만, 전역 이름의 가려짐은 해결할 수 없다.
// 따라서 전역 operator new/delete의 wrapper interface를 만듬으로서 해결한다.
// 좀 더 편하게 하려면 wrapper interface들을 가지는 클래스를 하나 만들고 이걸 상속함으로서 해결한다.
namespace name_hiding {
namespace problem {
class Example {
public:
using ::operator new; // compile error. it is impossible :(
static void* operator new(std::size_t size, void *pMem) noexcept {
...
}
};
void situation() {
new Example(); // compile error.
}
}
namespace solution {
class GlobalNewDeleteFeature { ... }; // it has wrapper interfaces to global operator new/delete
class Example : public GlobalNewDeleteFeature {
public:
using GlobalNewDeleteFeature::operator new;
using GlobalNewDeleteFeature::operator delete;
static void* operator new(std::size_t size, void *pMem) noexcept {
...
}
};
void situation() {
new Example(); // compile ok.
}
}
}
// new 키워드로 객체를 생성할 때, 생성자에서 예외가 발생할 경우, operator delete가 호출되어야만 한다.
// 메모리할당시 사용된 operator new와 "추가 매개변수" 개수/타입이 똑같은 operator delete을 찾아서 호출한다.
// 만약, 그러한 operator delete가 존재하지 않는다면 아무것도 호출되지않고, memory leak이 발생하게 된다.
// 해결책은 항상 operator new와 operator delete는 짝이 맞게 정의하는 것이다.
// 추가적으로 new키워드와 delete키워드 또한 항상 짝이 맞게 사용해주어야한다. 그렇지 않을경우 문제가 생길 수 있다.
namespace memory_leak {
class Example {
public:
Example() {
throw 1;
}
static void* operator new(std::size_t size, void *pMem) noexcept {
return ::operator new(size, pMem);
}
};
void situation() {
char buf[sizeof(Example)];
auto ptr = new (buf) Example(); // operator delete is not called.
delete ptr; // call basic operator delete(="void ::operator delete(void*)").
}
}
[이것만은 잊지 말자]
- operator new 함수의 위치지정 버전을 만들 대는, 이 함수와 짝을 이루는 위치지정 버전의 operator delete 함수도 꼭 만들어 주세요. 이 일을 빼먹었다가는, 찾아내기도 힘들며 또 생겼다가 안 생겼다 하는 메모리 누출 현상을 경험하게 됩니다.
- new 및 delete의 위치지정 버전을 선언할 때는, 의도한 바도 아닌데 이들의 표준 버전이 가려지는 일이 생기지 않도록 주의해 주세요.
빨리 new/delete를 죄다 커스터마이징해버리고 충동이 드는군요!! 여러분들도 같은 마음일꺼라 생각합니다!
댓글
댓글 쓰기