- operator new 함수의 위치지정 버전을 만들 때는, 이 함수와 짝을 이루는 위치지정 버전의 oeprator delete 함수도 꼭 만들어야 한다. 그렇지 않으면, 메모리 누출이 생길 수 있다.
- new와 delete의 위치지정 버전을 선언할 때는, 의도한 것과 달리 표준 버전이 가려지는 일이 생기지 않도록 주의해야 한다.
위치지정 new & 위치지정 delete
다음과 같은 new표현식을 썼을 때 호출되는 함수는 두 개다.
Widget* pw = new Widget;
메모리 할당을 위해 operator new가 호출되고, 그 뒤를 이어 Widget의 기본 생성자가 호출된다.
여기서, 첫 번째 함수 호출은 무사히 지나갔는데 두 번째 함수 호출이 진행되다가 예외가 발생했다고 가정해 보자.
그렇다면 첫 단계에서 이미 끝난 메모리 할당을 어떻게 해서든 취소하지 않으면 안 된다.
그냥 두면 메모리 누출이 생길 것이기 때문이다.
사용자 코드에서는 이 메모리를 해제할 수 없다.
Widget 생성자에서 예외가 발생하면 pw에 포인터가 대입될 일은 절대로 안 생기기 때문이다.
따라서 메모리 해제는 C++ 런타임 시스템에 맡겨야 한다.
이때, C++ 런타임 시스템이 해 주어야 하는 일은 1단계에서 자신이 호출한 operator new 함수와 짝이 되는 버전의 operator delete 함수를 호출하는 것인데, 이것이 제대로 되려면 operator delete 함수들 가운데 어떤 것을 호출해야 하는지를 런타임 시스템이 제대로 알고 있어야 가능하다.
new/delete가 기본형 시그니처로 되어 있다면 이 부분은 신경 쓰지 않아도 된다.
기본형 new는 기본형 delete와 짝을 맞추기 때문이다.
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void* rawMemory) throw(); // 전역
void operator delete(void* rawMemory, std::size_t size) throw(); // 클래스
따라서 표준 형태의 new 와 delete만 사용하면, 런타임 시스템은 new의 동작을 되돌릴 방법을 알고 있는 delete를 찾아내는 데 있어서 아무런 고민을 하지 않는다.
그런데 operator new의 기본형이 아닌 형태를 선언하면 짝을 맞추기 어렵게 된다.
예를 들어보면, 어떤 클래스에 대해 전용으로 쓰이는 operator new를 만들고 있다.
메모리 할당 정보를 로그로 기록해 줄 ostream을 지정받는 꼴로 만든다고 가정해 보자.
그리고 클래스 전용 operator delete는 기본형으로 만든다고 가정하자.
class Widget {
public:
...
static void* operator new(std::size_t size, std::otream& logStream) throw(std::bad_alloc);
static void operator delete(void* pMemory, size_t size) throw();
...
};
operator new 함수는 기본형과 달리 매개변수를 추가로 받는 형태로도 선언할 수 있다.
이런 형태의 함수를 위치지정 new라고 한다.
위치지정 new는 개념적으로 추가 매개변수를 받는 new이므로 위치지정 new는 다양할 수 있지만, 이들 중 특히 유용한 것이 하나 있다.
바로 객체를 생성시킬 메모리 위치를 나타내는 포인터를 매개변수로 받는 것이다.
void* operator new(std:;size_t, void* pMemory) throw();
이렇게 포인터를 추가로 받는 형태의 위치지정 new는 C++ 표준 라이브러리의 일부로도 들어가 있다.
<new>를 include하면 사용할 수 있다.
이 버전의 new 함수는 표준 라이브러리의 여러 군데에서 쓰이고 있는데, 특히 vector의 경우 해당 벡터의 미사용 공간에 원소 객체를 생성할 때 이 위치지정 new를 쓰고 있다.
위에 있는 Widget 클래스를 보면 설계적으로 문제가 있다.
Widget객체 하나를 동적으로 할당할 때 cerr에 할당 정보를 로그로 기록하는 코드이다.
Widget *pw = new (std::cerr) Widget; // cerr을 ostream인자로 넘김, Widget 생성자에서 예외가 발생하면 메모리 누출
메모리 할당은 성공했지만 Widget 생성자에서 예외가 발생했을 경우, operator new에서 저지른 할당을 되돌리는 일은 C++런타임 시스템이 책임지고 해야 한다.
그런데 런타임 시스템 쪽에는 호출된 operator new가 어떻게 동작하는지를 알아낼 방법이 없으므로, 자신이 할당 자체를 되돌릴 수는 없다.
그 대신, 런타임 시스템은 호출된 operator new가 받아들이는 매개변수의 개수 및 타입이 똑같은 버전의 operator delete를 찾고, 찾아냈다면 호출한다.
지금 경우에 호출된 operator new는 ostream& 타입의 매개변수를 추가로 받아들이므로, 이것과 짝을 이루는 operator delete도 똑같은 시그니처를 가진 것이 마련되어 있어야 한다.
void operator delete(void*, std::ostream&) throw();
매개변수를 추가로 받아들인다는 면에서 위치지정 new와 비슷하므로, 이런 형태의 oeprator delete를 가리켜 위치지정 delete 라고 한다.
그런데 지금의 Widget에는 operator delete의 위치지정 버전이 마련되어 있지 않기 때문에, 런타임 시스템 쪽에서는 위치지정 new로 할당한 메모리를 어떻게 되돌려야 할지 모른다.
그렇기 때문에 아무것도 하지 못한다.
즉, 앞에서 본 코드에서 Widget 생성자가 예외를 던지면 어떤 operator delete도 호출되지 않는다는 뜻이다.
규칙은 단순하다.
추가 매개변수를 취하는 operator new 함수가 있는데 그것과 똑같은 추가 매개변수를 받는 operator delete가 짝으로 존재하지 않으면, 이 new에 해당 매개변수를 넘겨서 할당한 메모리를 해제해야 하는 상황이 오더라도 어떤 operator 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 생성자에서 예외가 발생되더라도, 위치지정 new와 짝이 되는 위치지정 delete가 자동으로 호출된다.
Widget* pw = new(std::cerr) Widget; // 메모리 누출 없음
이제는 위치지정 new와 짝이 되는 위치지정 delete가 자동으로 호출된다.
그런데 위의 문장에서 Widget 생성자가 예외를 던지지 않았고 사용자 코드의 delete문에 도달하면 기본형의 operator delete가 호출된다.
delete pw;
위치지정 delete가 호출되는 경우는 위치지정 new의 호출이 되는 생성자에서 예외가 발생할 때뿐이다.
즉, 포인터에 delete를 적용했을 때는 절대로 위치지정 delete가 호출되지 않는다.
어떤 위치지정 new 함수와 연관된 모든 메모리 누출을 방지하려면, 표준 형태의 operator delete를 기본으로 마련해 두어야 하고 그와 함께 위치지정 new와 똑같은 추가 매개변수를 받는 위치지정 delete도 준비해야 한다.
빼먹지 말아야 하는 부분이 하나 있다.
바깥쪽 유효범위에 있는 어떤 함수의 이름과 클래스 멤버 함수의 이름이 같으면 바깥쪽 유효범위의 함수가 가려지게 되어 있다.
그렇기 때문에 다른 new들을 클래스 전용의 new가 가리지 않도록 각별히 신경을 써야 한다.
예를 들면, 위치지정 new만 선언된 기본 클래스가 제공될 경우, 사용자 쪽에서는 표준 형태의 new를 사용하지 못한다.
class Base {
public:
...
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
...
};
Base* pb = new Base; // Error, 표준 operator new가 가려짐
Base* pb = new(std::cerr) Base; // OK
이런 기본 클래스에서 파생된 클래스는 더욱 문제가 된다.
전역 operator new는 물론이고 자신이 상속받은 기본 클래스의 operator new까지 가려 버린다.
class Derived: public Base {
public:
...
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
};
Derived* pd = new(std::clog) Derived; // Error, Base의 위치지정 new가 가려짐
Derived* pd = new Derived; // OK
C++이 전역 유효범위에서 제공하는 operator new의 형태는 다음의 세 가지가 표준이라는 점을 기억해야 한다.
void* operator new(std::size_t) throw(std::bad_aloc); // 기본형 new
void* operator new(std::size_t, void*) throw(); // 위치지정 new
void* operator new(std::size_t, const std::nothrow_t&) throw(); // 예외불가 new
어떤 형태이든 간에 operator new가 클래스 안에 선언되는 순간, 표준 형태들이 모두 가려진다.
사용자가 표준 형태를 쓰지 못하게 막을 의도가 아니었다면, operator new 형태 외에 표준 형태들도 사용자가 접근할 수 있도록 열어주어야 한다.
클래스 안에 할당, 해제 함수들이 똑같은 방식으로 동작했으면 하는 경우에는, 클래스 전용 버전이 전역 버전을 호출하도록 구현해 두면 된다.
그중 한 가지 방법으로는 클래스 한 개를 만들고, 이 안에 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); }
};
표준 형태에 덧붙여 사용자 정의 형태를 추가하고 싶다면, 기본 클래스를 축으로 넓혀 가면 된다.
상속과 using 선언을 사용해서 표준 형태를 파생 클래스 쪽으로 끌어와 외부에서 사용할 수 있게 만든 후, 원하는 사용자 정의 형태를 선언하면 된다.
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); // 사용자 정의 위치지정 new
static void* operator delete(std::size_t size, std::ostream& logStream) throw(); // 사용자 정의 위치지정 delete
...
};