로또
52. 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자. 본문
다루는 내용
위치지정 new/delete가 무엇인지, new와 delete를 짝지어주어야 하는 이유가 무엇인지 알아보고 이름 가림을 유용하게 해결하는 방법에 대해서도 알아보자.
위치지정 new란?
operator new 함수의 기본적인 형태는 다음과 같다.
출간된 지 오래된 책이라 throw와 관련하여 문법적인 차이가 있음을 명심하자.
void* operator new(std::size_t) throw(std::bad_alloc);
operator new 함수는 기본형과 달리 매개변수를 추가로 받는 형태로도 선언할 수 있다.
이러한 형태의 함수를 위치지정(placement) new 라고 일컫는다. 위치지정 new는 개념적으로 "추가 매개변수를 받는 new"이다.
매개변수를 추가로 받는 new 버전 중 C++ 표준 라이브러리의 일부에 속할 정도로 유용한 함수가 있다. 어떤 객체를 생성시킬 메모리 위치를 나타내는 포인터를 추가 매개변수로 받는 것이다.
void* operator new(std::size_t, void* p_memory) throw();
이것이 바로 위치지정 new의 원조로, 추가 매개변수를 받는 new를 위치지정 new라고 불리게 만든 장본인이다.
위치지정 new와 위치지정 delete를 함께 준비해야 하는 이유
Widget* pw = new Widget;
위와 같은 new 표현식에서는 함수 두 개가 호출된다. 메모리 할당을 위한 operator new 함수와 Widget의 생성자이다.
operator new 함수가 성공한 후, Widget 생성자에서 예외가 발생했다고 생각해보자. 이 때 할당된 메모리를 해제해주지 않는다면 메모리 누출이 발생할 것이다. 예외가 발생한 상황에서 사용자 코드에서는 할당된 메모리에 접근할 수 있는 방법이 없기에 C++ 런타임 시스템에서 이를 해결해준다.
C++ 런타임 시스템은 이러한 상황이 발생할 경우, 자신이 호출한 operator new 함수와 짝이 되는 버전의 operator delete 함수를 호출한다. 이를 위해서는 여러 operator delete 중에 어떤 것을 호출해야하는지 런타임 시스템이 알고 있어야 한다.
new와 delete 짝맞추기
앞서 다룬 예외가 발생한 상황에서, C++ 런타임 시스템이 올바른 delete를 호출할 수 있도록 도와주기 위해 우리는 operator new가 받아들이는 매개변수의 개수 및 타입이 똑같은 버전의 operator delete를 작성해주어야 한다.
기본형 operator new는 기본형 operator delete와 다음과 같이 짝을 맞추고 있다.
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void* raw_memory) throw(); // 전역 유효범위에서 기본형 시그니처
void operator delete(void* raw_memory, std::size_t size) throw(); // 클래스 유효범위에서 기본형 시그니처
Widget 클래스의 메모리 할당 정보를 기록해 줄 ostream을 매개변수로 받는 operator new를 만들었을 때, operator delete의 짝은 다음과 같이 맞추어줄 수 있다.
class Widget{
public:
static void* operator new(std::size_t size, std::ostream& log_stream) throw(std::bad_alloc); // 위치지정 new
static void operator delete(void* p_memory, std::ostream& log_stream) throw(); // 위치지정 delete
static void operator delete(void* p_memory) throw(); // 기본형 delete
};
만약 이러한 짝이 없을 경우, operator new 이후 호출되는 생성자에서 예외가 발생한다면 그 어떤 operator delete도 호출되지 않으므로 메모리 누출이 발생한다.
생성자에서 예외가 발생하지 않아 정상적으로 프로그램이 실행되고, 이후 사용자 코드의 delete까지 다다랐다면 C++ 런타임 시스템은 기본형 operator delete를 호출할 것이다. 위치지정 delete가 호출되는 경우는 짝꿍인 위치지정 new의 호출에 의해 호출되는 생성자에게 예외가 발생할 때뿐이다. 포인터에 delete를 적용했을 때는 절대로 위치지정 delete를 호출하지 않고 기본형 operator delete를 호출한다.
Widget* pw = new (std::cerr) Widget; // 위치지정 new 호출
...
delete pw; // 기본형 delete 호출
즉, 어떤 위치지정 new 함수와 연관된 모든 메모리 누출을 사전에 봉쇄하려면 객체 생성 도중에 예외가 던져지지 않았을 경우에 대비해서 위치지정 delete뿐만 아니라 표준 형태의 operator delete도 기본으로 마련해두어야 한다.
new와 delete의 이름 가림
바깥쪽 유효범위에 있는 어떤 함수의 이름과 클래스 멤버 함수의 이름이 같으면 바깥쪽 유효범위의 함수가 가려진다.
class Base{
public:
static void* operator new(std::size_t size, std::ostream& log_stream) throw (std::bad_alloc);
...
};
Base* pb = new Base; // Error! 이름 가림에 의해 표준 버전 new 호출 불가
Base* pb = new (std::cerr) Base; // OK! Base의 위치지정 new 호출
특히, 위와 같은 기본 클래스를 상속한 파생 클래스는 전역 operator new는 물론이고 기본 클래스의 operator new까지 가려버린다.
class Base{
public:
static void* operator new(std::size_t size, std::ostream& log_stream) throw (std::bad_alloc);
...
};
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! Derived의 new 호출
우리는 표준 버전을 포함한 new들을 클래스 전용의 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
어떤 형태이든 간에 operator new가 클래스 안에 선언되는 순간 위의 표준 형태들이 몽땅 가려진다.
이러한 상황을 간단하게 타개하기 위해 클래스 전용 버전이 전역 버전을 호출하도록 구현해두면 된다. 또한 이에 덧붙여 사용자 정의 형태를 추가하고 싶다면 상속과 using 선언을 사용하여 표준 형태를 파생 클래스쪽으로 끌어와 외부에서 사용할 수 있도록 만들어준 후, 원하는 사용자 정의 형태를 추가해주자.
class StandardNewDeleteForms{
public:
// 기본형 new/delete
static void* operator new(std::size_t) throw(std::bad_alloc)
{ return ::operator new(size); }
static void operator delete(void* p_memory) throw()
{ ::operator delete(p_memory); }
// 위치지정 new/delete
static void* operator new(std::size_t, void* ptr) throw()
{ return ::operator new(size_t, ptr); }
static void operator delete(void* p_memory, void* ptr) throw()
{ ::operator delete(p_memory, ptr); }
// 예외불가 new/delete
static void* operator new(std::size_t, const std::nothrow_t& nt) throw()
{ return ::operator new(size_t, nt); }
static void operator delete(void* p_memory, const std::nothrow_t& nt) throw()
{ ::operator delete(p_memory); }
};
class Widget: public StandardNewDeleteForms{
public:
// 표준 형태가 Widget 내부에 보이도록 만든다.
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
// 사용자 정의 위치지정 new/delete를 추가한다.
static void* operator new(std::size_t size, std::ostream& log_stream) throw(std::bad_alloc);
static void* operator delete(void* p_memory, std::ostream& log_stream) throw();
...
};
이것만은 잊지 말자!
- operator new 함수의 위치지정 버전을 만들 때는, 이 함수와 짝을 이루는 위치지정 버전의 operator delete 함수도 꼭 만들어주자. 이를 빼먹었다가는, 찾아내기도 힘들며 또 생겼다가 안 생겼다 하는 메모리 누출 현상을 경험하게 된다.
- new 및 delete의 위치지정 버전을 선언할 때는, 의도한 바도 아닌데 이들의 표준 버전이 가려지는 일이 생기지 않도록 주의하자.
'책 > Effective C++' 카테고리의 다른 글
| 53. 컴파일러 경고를 지나치지 말자. (1) | 2024.01.06 |
|---|---|
| 51. new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 (1) | 2024.01.03 |
| 50. new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자. (1) | 2024.01.02 |
| 49. new 처리자의 동작 원리를 제대로 이해하자. (1) | 2023.12.28 |
| 44. 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (0) | 2023.12.15 |