51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자
operator new를 구현 하려면 다음의 요구사항만큼은 기본으로 지켜야 한다.
- 반환 값이 제대로 되어있어야 한다.
- 가용 메모리가 부족할 경우에는 new 처리자 함수를 호출해야 한다.
- 크기가 없는(0바이트) 메모리 요청에 대한 대비책을 갖춰야 한다.
- 실수로 "기본(normal)" 형태의 new가 가려지지 않도록 해야 한다.
반환 값 부분은 간단하다. 요청된 메모리를 마련해 줄 수 있으면 그 메모리에 대한 포인터를 반환하는 것으로 끝이다. 메모리를 마련해 줄 수 없을 때는, 항목 49에서 이야기한 규칙을 따라서 bad_alloc 타입의 예외를 던지게 하면 된다.
지금까지의 요구사항을 정리해서, 비멤버 버전의 operator new 함수를 의사 코드로 만들어 보면 다음과 같다.
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
// 0바이트 요청이 들어오면, 이것을 1바이트 요구로 간주하고 처리한다.
if (size == 0)
size = 1;
while (true)
{
size바이트 할당 시도;
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 처리자 함수를 얻어오는 직접적인 방법이 없다. 위와 같이 할 수 밖에 없다. 단일 스레드에서 동작하는 환경이라면 이렇게 해도 된다. 반면, 다중스레드 환경에서는 new 처리자 함수를 둘러싼 (전역) 자료구조들이 조작될 때 스레드 안정성이 보장되어야 하기 때문에 스레드 잠금을 걸어야 한다.
operator new 함수에는 무한 루프가 들어있다. 바로 "while (true)"이다. 이 루프를 빠져나오는 유일한 조건은 메모리 할당이 성공하든지 아니면 항목 49에서 이야기한 동작들 중 한 가지를 new 처리자 함수 쪽에서 해 주든지 둘 중 하나이다.
사실, operator new 멤버 함수는 파생 클래스 쪽으로 상속이 되는 함수이다. 특정 클래스 전용의 할당자를 만들어서 할당 효율을 최적화하기 위해서 사용자 정의 메모리 관리자를 작성할 수 있다는 이야기를 항목 50에서 했다. 여기서 특정 클래스란 '그' 클래스 하나를 가리킬 뿐, '그 클래스 혹은 그 클래스로부터 파생된 다른 클래스들' 모두를 통칭하는 것은 아니다. 즉, 크기가 sizeof(Base)인 객체에 대해 맞추어져 있는 것이다.
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가 호출된다!
가장 좋은 해결 방법은 "틀린" 메모리 크기가 들어왔을 때를 시작부분에서 확인한 후에 표준 operator new를 호출하는 쪽으로 살짝 비껴가게 만드는 것이다.
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base)) // 틀린 크기라면 표준 operator new
return ::operator new(size);
}
여기서 0바이트 점검은 sizeof(Base)와 size를 비교하는 코드에 포함되어 있다. C++에는 모든 독립 구조(freestanding)의 객체는 반드시 크기가 0이 넘어야 한다는 요상한 금기사항 같은 것이 있다(항목 39 참조).
배열에 대한 메모리 할당을 클래스 전용 방식으로 하고 싶다면, operator new의 사촌격인 operator new[ ]함수를 구현하면 된다. operator new[ ] 안에서 해 줄 일은 단순히 원시 메모리의 덩어리를 할당하는 것밖에 없다. 배열 안에 몇 개의 객체가 들어갈지 계산하는 것조차 안 된다. 그 이유는 다음과 같다.
- 객체 하나가 얼마나 큰지를 확정할 방법이 없다.
- operator new[ ]에 넘어가는 size_t 타입의 인자는 객체들을 담기에 딱 맞는 메모리 양보다 더 많게 설정되어 있을 수도 있다.
operator delete에서 지킬 관례는 더 간단하다. C++는 널 포인터에 대한 delete 적용이 항상 안전하도록 보장한다는 사실만 잊지 않으면 된다.
void operator delete(void* rawMemory) throw()
{
if (rawMemory == 0) return;
rawMemory가 가리키는 메모리를 해제한다;
}
클래스 전용 버전도 단순하다. 클래스 전용의 operator delete 역시 "틀린 크기로 할당된" 메모리의 삭제 요청을 ::operator delete 쪽으로 전달하는 식으로 구현하면 된다.
void Base::operator delete(void* rawMemory, std::size_t size) throw()
{
if (rawMemory == 0) return;
if (size != sizeof(Base)) {
::operator delete(rawMemory);
return;
}
rawMemory가 가리키는 메모리를 헤제한다;
return;
}
마지막으로 가상 소멸자가 없는 기본 클래스로부터 파생된 클래스의 객체를 삭제하려고 할 경우에는 operator delete로 C++가 넘기는 size_t 값이 엉터리일 수 있다. 이것만으로 기본 클래스에 가상 소멸자를 꼭 두어야 하는 충분한 이유가 된다(항목 7).
이것만은 잊지 말자!
- 관례적으로, operator new 함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0바이트에 대한 대책도 있어야 한다. 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰(틀린) 메모리 블록에 대한 요구도 처리해야 한다.
- operator delete 함수는 널 포인터가 들어왔을 때 아무 일도 하지 않아야 한다. 클래스 전용 버전의 경우에는 예정 크기보다 더 큰 블록을 처리해야 한다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 53: 컴파일러 경고를 지나치지 말자 (1) | 2023.11.25 |
---|---|
[Effective C++] 52: 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자 (0) | 2023.11.24 |
[Effective C++] 50: new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자 (1) | 2023.11.21 |
[Effective C++] 49: new 처리자의 동작 원리를 제대로 이해하자 (1) | 2023.11.21 |
[Effective C++] 48: 템플릿 메타프로그래밍, 하지 않겠는가? (0) | 2023.11.18 |