52: 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자
Widget* pw = new Widget;
위의 코드는 항목 17에서도 말했듯이, operator new와 Widget의 기본 생성자, 두 개의 함수가 실행된다. 여기서 두 번째 함수 호출이 진행되가가 예외가 발생했다고 가정하자. 이 경우 이미 끝난 메모리 할당을 취소하지 않으면 안된다. 사용자 코드에서는 이 메모리를 해제할 수 없다. 따라서 1단계의 메모리 할당을 안전하게 되돌리는 중대 임무는 C++ 런타임 시스템이 맡아 준다.
C++ 런타임 시스템이 해 주어야 하는 일은 operator new와 짝이 맞는 버전의 operator delete를 호출하는 것이다. 우리가 사용하고 있던 기본 new, delete는 그다지 대수로운 사안이 아니다. 왜냐하면 기본형 operator new는 기본형 operator delete와 짝을 맞추기 때문이다.
그러나 기본형이 아닌 형태를 선언한다면 문제가 된다. 비기본형이란 다른 매개변수를 추가로 갖는 operator new를 뜻한다. 매개변수를 추가로 받는 형태의 operator new를 위치지정(placement) new라고 한다. 예를 하나 보자.
class Widget {
public:
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); // 비표준 형태
static void operator delete(void* pMemory, size_t size) throw(); // 클래스 전용 opeartor delete의 표준 형태
};
Widget* pw = new (std::cerr) Widget; // Widget 생성자에서 예외가 발생하면 메모리가 누출된다.
메모리 할당은 성공했지만 Widget 생성자에서 예외가 발생했을 경우, 앞에서의 할당은 C++ 런타임 시스템이 책임져야 한다. 그런데, 런타임 시스템 쪽에는 호출된 operator new가 어떻게 동작하는지를 알아낼 방법이 없다. 런타임 시스템은 호출된 operator new가 받아들이는 매개변수의 개수 및 타입의 똑같은 버전의 operator delete를 찾고, 찾아냈으면 그 녀석을 호출한다.
void operator delete(void*, std::ostream&) throw();
이런 형태의 operator delete를 가리켜 위치지정 delete라고 한다.
결국, 위치지정 new와 짝이 되는 위치지정 delete를 Widget클래스에 넣어 주어야 한다는 결론이 나온다.
class Widget {
public:
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void* pMemory) throw();
static void operator delete(void* pMemory, std::ostream& logStream) throw();
};
이제 Widget 생성자에서 예외가 발생해도, 자동으로 위치지정 delete가 호출된다.
그런데 위의 문장에서 Widget 생성자가 예외를 던지지 않았고 사용자 코드의 delete 문까지 다다랐다고 하면 어떨까?
delete pw; // 기본형의 operator delete가 호출된다.
이 경우에는 런타임 시스템이 기본형의 operator delete를 호출한다. 포인터에 delete를 적용했을 때는 절대로 위치지정 delete를 호출하는 쪽으로 가지 않는다. 때문에 사용자 자신이 쓸 수 있다고 생각하는 다른 new들(표준 버전을 포함해서)을 클래스 전용의 new가 가리지 않도록 가별히 신경을 써야 한다.
class Base {
public:
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
};
Base* pb = new Base; // 에러! 표준 형태의 전역 operator new가 가려진다.
Base* pb = new (std::cerr) Base; // 문제 없음
여기서 파생 클래스는 한 술 더 뜬다. 전역 operator new는 물론이고 자신이 상속받은 기본 클래스의 operator new까지 가려 버린다.
class Derived : public Base {
public:
static void* operator new(std::size_t) throw(std::bad_alloc); // 기본형 new를 클래스 전용으로 다시 선언
};
Derived* pd = new(std::clog) Derived; // 에러! Base의 위치지정 new가 가려져 있다.
Derived* pd = new Derived; // 문제 없음. Derived의 operator new를 호출한다.
기본적으로 C++가 전역 유효 범위에서 제공하는 operator new의 형태는 다음의 세 가지가 표준이다.
void* operator new(std::size_t) throw(std::bad_alloc); // 기본형 new
void* operator new(std::size_t, void*) throw(); // 위치지정 new
void* operator new(std::size_t, const std::nothrow_t& throw(); // 예외불가 new(항목 49)
어떤 형태이든 간에 operator new가 클래스 안에 선언되는 순간, 위의 표준 형태들이 몽땅 가려진다. 표준 형태들도 사용자가 접근할 수 있도록 길을 열어주도록 하자. 물론, operator new를 만들었다면 바늘에 실 따라가듯 operator delete도 만들어 주는 것도 잊지 말자.
이것들을 쉽게 할 수 있는 방법을 알아보자. 기본 클래스 하나를 만들고, 이 안에 new 및 delete의 기본 형태를 전부 넣어두자.
class StandardNewDeleteForms {
public:
// 기본형 new/delete
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); }
// 위치지정 new/delete
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); }
// 예외불가 new/delete
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& nt) throw()
{ ::operator delete(pMemory, nt); }
};
표준 형태에 덧붙여 사용자 정의 형태를 추가하고 싶다면, 이제는 이 기본클래스를 축으로 넓혀 가면 된다.
class Widget : public StandardNewDeleteForms {
public:
using StandardNewDeleteForms::operator new; // 표준 형태가 보이도록 만든다.
using StandardNewDeleteForms::operator 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();
};
이것만은 잊지 말자!
- operator new 함수의 위치지정(placement) 버전을 만들 때는, 이 함수와 짝을 이루는 위치지정 버전의 operator delete 함수도 꼭 만들어 주자. 이 일을 빼먹었다가는, 찾아내기도 힘들며 또 생겼다가 안 생겼다 하는 메모리 누출 현상을 경험하게 된다.
- new 및 delete이 위치지정 버전을 선언할 때는, 의도한 바도 아닌데 이들의 표준 버전이 가려지는 일이 생기지 않도록 주의하자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 54: TR1을 포함한 표준 라이브러리 구성요소와 편안한 친구가 되자 (1) | 2023.11.26 |
---|---|
[Effective C++] 53: 컴파일러 경고를 지나치지 말자 (1) | 2023.11.25 |
[Effective C++] 51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 (0) | 2023.11.22 |
[Effective C++] 50: new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자 (1) | 2023.11.21 |
[Effective C++] 49: new 처리자의 동작 원리를 제대로 이해하자 (1) | 2023.11.21 |