로또
51. new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 본문
다루는 내용
operator new와 delete를 작성할 때 반드시 지켜야 하는 점을 알아보자. 또한 operator new와 delete에 대한 클래스 버전을 만들 때 주의해야 할 점을 다룬다.
operator new의 요구사항
- 제대로 된 반환값을 가져야 한다.
- 가용 메모리가 부족할 경우에는 new 처리자 함수를 호출해야 한다.
- 0바이트 메모리 요청에 대한 대비책을 갖춰두어야 한다.
- 기본 형태의 new가 가려지지 않아야 한다.
이러한 요구 사항을 의사 코드로 나타내면 다음과 같다.
void* operator new(std::size_t size)
{
using namespace std;
if(size == 0) size = 1;
while(true){
// size byte 할당 성공
if(할당 성공) return 할당된 메모리에 대한 포인터;
// 할당 실패
// 현재의 new 처리자 함수 확인
// set_new_handler는 기존의 handler 함수를 반환한다.
new_handler global_handler = set_new_handler(0);
set_new_handler(global_handler);
if(global_handler) (*global_handler)();
else throw std::bad_alloc();
}
}
operator new는 메모리 할당이 실패할 때마다 new 처리자 함수를 호출하는 식으로 메모리 할당을 2회 이상 시도한다. operator new가 예외를 던지는 경우는 오직 new 처리자 함수에 대한 포인터가 null일 때 뿐이다.
앞서 다룬 "49. new 처리자의 동작 원리를 제대로 이해하자."에서 new 처리자가 해야 할 일을 다루었다. 가용 메모리를 늘리든가, 다른 new 처리자를 설치하든가, new 처리자를 제거하든가, bad_alloc 계열의 예외를 던지든가, 함수 복귀를 포기하고 도중 중단을 시켜야 한다. 이 중 하나를 하지 않는다면 operator new 함수는 무한 루프에 빠져버림을 위 코드를 통해 알 수 있다.
클래스 전용의 operator new
operator new 멤버 함수는 파생 클래스에게 상속이 되는 함수이므로 주의할 점이 있다.
특정 클래스 전용의 operator new를 만든다고 가정해보자.
class Base{
public:
static void* operator new(std::size_t size);
...
};
class Derived: public Base{...}; // operator new가 존재하지 않음.
Derived* p = new Derived; // Base::operator new 호출
Base 클래스 전용의 operator new에서는 이런 상황에 대해 적절한 조치를 취해주어야 한다.
그 예로, 틀린 메모리 크기가 들어왔을 때, 표준 operator new를 호출하도록 만들어주는 것이다.
void* Base::operator new(std::size_t size)
{
if(size != sizeof(Base))
return ::operator new(size);
...
}
이 때, 앞서 언급했던 0바이트 점검 코드가 사라진 것처럼 보일 수 있다.
하지만 C++에서는 모든 독립 구조 객체는 반드시 크기가 0이 넘어야 한다는 사항이 존재한다. 때문에 sizeof(Base)는 절대 0이 될 수 없으며, size가 0이면 기본 버전의 operator new가 호출되어 적절한 처리를 해줄 것이다.
클래스 전용의 operator new [ ]
operator new[]를 직접 구현할 수도 있으나, 다음 이야기를 잊지 말자.
operator new[] 안에서 해 줄 일은 단순히 원시 메모리의 덩어리를 할당하는 것밖에 없다. 이 시점에서는 배열 메모리에 아직 생기지도 않은 클래스 객체에 대해 아무것도 할 수 없기 때문이다.
앞서 언급한 상속으로 인해 파생 클래스 객체의 배열을 할당하는 데 기본 클래스의 operator new[] 함수가 호출될 수 있고 일반적으로 파생 클래스 객체는 기본 클래스 객체보다 크다. 때문에 Base::operator new[] 에서 배열에 들어가는 객체 하나의 크기가 sizeof(Base)라고 가정할 수 없다.
또한, operator new[]에 전달되는 size_t 타입의 인자는 객체들을 담기에 딱 맞는 메모리 양보다 더 많게 설정될 수 있다. 동적으로 할당된 배열 원소의 개수를 담기 위한 공간을 추가적으로 필요로 하기 때문이다.
operator delete의 요구사항
C++은 널 포인터에 대한 delete 적용이 항상 안전하도록 보장한다. 이 사실만 잊지 말자. 우리는 operator delete를 작성할 때 이 사실만 기억하면 된다.
아래는 비멤버 버전 delete의 간단한 예시 코드이다.
void operator delete(void* raw_memory)
{
if(raw_memory == 0) return;
...
// raw_memory가 가리키는 메모리 해제
}
클래스 전용의 operator delete
클래스 전용 delete는 비멤버 delete와 달리 삭제될 메모리의 크기를 점검하는 코드를 넣어주어야 한다.
클래스 전용 버전의 operator new가 틀린 크기의 메모리 요청을 ::operator new로 넘기도록 구현되었다고 가정하면, 클래스 전용 버전의 operator delete 역시 틀린 크기로 할당된 메모리의 삭제 요청을 ::operator delete로 넘기도록 구현할 수 있을 것이다.
class Base{
public:
static void* operator new(std::size_t size);
static void operator delete(void* raw_memory, std::size_t size);
};
void Base::operator delete(void* raw_memory, std::size_t size)
{
if(raw_memory == 0) return;
if(size != sizeof(Base)){
::operator delete(raw_memory);
return;
}
// raw_memory 해제
...
}
또한, 기본 클래스에서 소멸자를 가상으로 선언하는 것을 잊으면 operator delete 함수가 똑바로 동작하지 않을 수 있다.
가상 소멸자가 없는 기본 클래스로부터 파생된 클래스의 객체를 삭제하려고 할 경우에는, operator delete로 C++가 넘기는 size_t 값이 엉터리일 수 있기 때문이다.
이것만은 잊지 말자!
- 관례적으로, operator new 함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0바이트에 대한 대책도 있어야 합니다. 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰(틀린) 메모리 블록에 대한 요구도 처리해야 합니다.
- operator delete 함수는 널 포인터가 들어왔을 때 아무 일도 하지 않아야 합니다. 클래스 전용 버전의 경우에는 예정 크기보다 더 큰 블록을 처리해야 합니다.
'책 > Effective C++' 카테고리의 다른 글
| 53. 컴파일러 경고를 지나치지 말자. (1) | 2024.01.06 |
|---|---|
| 52. 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자. (2) | 2024.01.04 |
| 50. new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자. (1) | 2024.01.02 |
| 49. new 처리자의 동작 원리를 제대로 이해하자. (1) | 2023.12.28 |
| 44. 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (0) | 2023.12.15 |