- 관례적으로, operator new 함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0바이트에 대한 대책도 있어야 한다. 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰 메모리 블록에 대한 요구도 처리해야 한다.
- operator delete 함수는 널 포인터가 들어왔을 때 아무 일도 하지 않아야 한다. 클래스 전용 버전의 경우에는 예정 크기보다 더 큰 블록을 처리해야 한다.
new와 delete 작성 관례
사용자 정의 new와 delete를 작성할 때 지켜야 하는 관례가 있다.
우선, operator new를 보면 요구사항 몇 가지를 지켜야 한다.
반환 값이 제대로 되어 있어야 하고, 가용 메모리가 부족할 경우에는 new 처리자 함수를 호출해야 하며, 크기가 없는 메모리 요청에 대한 대비책을 갖춰두어야 한다.
또한, 기본 형태의 new가 가려지지 않도록 해야 한다.
operator new의 반환 값 부분은 간단하다.
요청된 메모리를 마련해 줄 수 있으면 그 메모리에 대한 포인터를 반환한다.
메모리를 마련해 줄 수 없는 경우가 문제인데, 이 경우에는 bad_alloc 타입의 예외를 던지게 된다.
하지만, 구현은 이렇게 간단하지 않다.
operator new는 메모리 할당이 실패할 때마다 new 처리자 함수를 호출하는 식으로 메모리 할당을 2회 이상 시도하기 때문이다.
즉, 메모리를 해제하는 동작을 new 처리자 함수 쪽에서 할 수 있을 것으로 가정하는 것이다.
operator new가 예외를 던지게 되는 경우는 오직 new 처리자 함수에 대한 포인터가 널일 때뿐이다.
그리고 0바이트가 요구되었을 때도 operator new 함수는 적법한 포인터를 반환해야 한다.
operator new 관례
void* operator new(std::size_t size) throw(std::bad_aloc)
{
using namespace std;
if(size ==0)
{
size = 1;
}
while(true)
{
if(할당 성공) return (할당된 메모리에 대한 포인터);
// 할당 실패시, new 처리자
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
외부에서 0바이트를 요구했을 때 1바이트를 요구한 것으로 간주하고 처리하는 것은 이상해 보이지만 규칙에 어긋나지 않는다.
new 처리자 함수의 포인터를 널로 설정하고 바로 뒤에 원래의 처리자 함수로 되돌려 놓는 코드도 보인다.
현재의 전역 new 처리자 함수를 얻어오는 직접적인 방법은 없고 set_new_handler 함수를 호출하고 그 반환 값을 가져오는 방법밖에 없다.
하지만, 이 방법은 단일 스레드 환경에서만 동작한다.
다중스레드 환경에서는 new 처리자 함수에 필요한 자료구조들이 조작될 때 스레드 안전성이 보장되어야 하기 때문에 스레드 잠금을 걸어야 한다.
operator new 함수에는 무한 루프가 들어 있다.
이 루프를 빠져나오는 조건은 메모리 할당이 성공하든지 아니면 new 처리자 함수 쪽에서 해주든지 둘 중 하나이다.
new 처리자 함수는 가용 메모리를 늘려 주든가, 다른 new 처리자를 설치하든가, new 처리자의 설치를 제거하든가, bad_alloc 타입의 예외를 던지든가, 함수 복귀를 포기하고 도중 중단을 시켜야 한다.
new 처리자가 동작을 하지 않으면, operator new 내부 루프는 절대로 끝나지 않는다.
operator new 멤버 함수는 파생 클래스 쪽으로 상속이 되는 함수이다.
operator new 함수의 의사 코드를 보면, 할당을 시도하는 메모리의 크기가 size바이트로 되어 있다.
만약 특정 클래스 전용의 할당자를 만들어서 할당 효율을 최적화하기 위해서 사용자 정의 메모리 관리자를 작성하려 한다면, 상속이 문제가 될 수 있다.
X라는 클래스를 위한 operator new 함수가 있다면, 이 함수의 동작은 sizeof(X)인 객체에 대해 맞추어져 있다.
그런데 이를 상속한 파생 클래스에서도 같은 operator new를 사용하게 된다.
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
};
class Derived: public Base { ... };
Derived *p = new Derived; // Base::operator new 호출
만약 Base 클래스 전용의 operator new 가 이런 상황에 대해 어떤 조치를 취하도록 설계되지 않았다면 전체 설계를 바꾸지 않고 쓸 수 있는 가장 좋은 해결 방법은 틀린 메모리 크기가 들어왔을 때를 시작부분에서 확인한 후에 표준 operator new를 호출하게 만드는 것이다.
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if(size != sizeof(Base)) return::operator new(size); // 틀린 크기 -> 표준 operator new
...
}
코드를 보면 0바이트를 점검하는 부분은 빠져있다.
사실 sizeof(Base)와 size를 비교하는 부분에 포함되어 있기 때문이다.
C++에는 모든 독립 구조의 객체는 반드시 크기가 0이 넘어야 한다.
그렇기 때문에, sizeof(Base)가 0이 될 일은 없다.
따라서 size가 0이면 if문이 거짓이 되어 메모리 처리 요구가 ::operator new 쪽으로 넘어가는 것이다.
만약 배열에 대한 메모리 할당을 클래스 전용 방식으로 하고 싶다면, operator new[] 함수를 구현하면 된다.
이때, operator new[]아네서 해 줄 일은 메모리의 덩어리를 할당하는 것밖에 없다.
배열 안에 몇 개의 객체가 들어갈지 계산하는 것도 할 수 없다.
객체의 크기가 얼마인지 확정할 방법이 없기 때문이다.
상속 때문에, 파생 클래스 객체의 배열을 할당하는 데 기본 클래스의 operator new[] 함수가 호출될 수 있다.
그리고 파생 클래스 객체는 대체적으로 기본 클래스 객체보다 더 크다는 것도 문제이다.
그렇기 때문에, Base::operator new[]안에서조차 배열에 들어가는 객체 하나의 크기가 sizeof(Base)라는 가정을 할 수 없다.
또한, operator new[]에 넘어가는 size_t 타입의 인자는 객체들을 담기에 딱 맞는 메모리 양보다 더 많게 설정되어 있을 수 있다.
동적으로 할당된 배열에는 배열 원소의 개수를 담기 위한 공간이 추가로 들어가기 때문이다.
operator delete 관례
operator delete의 관례는 operator new보다 간단하다.
C++은 널 포인터에 대한 delete 적용이 항상 안전하도록 보장한다는 사실만 알면 된다.
void operator delete(void* rawMemory) throw()
{
if(rawMemory == 0) return; // null 포인터 delete시 아무것도 하지 않음
// rawMemory 해제
}
operator delete의 클래스 전용 버전도 단순하다.
삭제될 메모리의 크기를 점검하는 코드를 넣어 주는 것만 추가하면 된다.
클래스 전용의 operator new가 틀린 메모리 요청을 ::operator new쪽으로 넘기도록 구현되었다고 가정하면, 클래스 전용의 operator delete 역시 틀린 크기로 할당된 메모리의 삭제 요청을 ::operator delete 쪽으로 전달하는 식으로 구현하면 된다.
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void* rawMemory, std:;size_t size) throw();
...
};
void Base:operator delete(void* rawMemory, std:;size_t size) throw()
{
if(rawMemory == 0) return; // nullptr 점검
if(size != sizeof(Base))
{
::operator delete(rawMemory); // 크기가 틀린경우 표준 operator delete가
return;
}
// rawMemory 해제
return;
}
가상 소멸자가 없는 기본 클래스로부터 파생된 클래스의 객체를 삭제하려고 할 경우에는 operator delete로 C++이 넘기는 size_t값이 엉터리일 수 있다.
즉, 가상 소멸자를 빼먹으면 operator delete 함수가 똑바로 동작하지 않을 수 있다.