스토리텔링 개발자

[Effective C++] 52. 위치지정 new / delete 본문

개발/Effective C++

[Effective C++] 52. 위치지정 new / delete

김디트 2024. 7. 26. 11:18
728x90

항목 52 : 위치 지정 new를 작성한다면 위치 지정 delete도 같이 준비하자.

 

 

 

new 중 기본 생성자에서 예외가 발생한다면?
Widget* pw = new Widget;
  • 위 코드는 실행 중 두 개의 함수가 순차적으로 호출된다. (항목 16, 항목 17 참조)
    1. 메모리 할당을 위한 operator new 호출
    2. Widget의 기본 생성자 호출
  • 만약 메모리 할당은 성공했으나, 기본 생성자에서 예외가 발생한다면?
    • 이미 할당된 메모리를 취소해야 한다.
    • 하지만 메모리에 대한 포인터가 pw에 할당되지 않고 예외가 발생했으므로, 사용자 코드에서는 메모리를 해제할 수 없다.
    • 그러므로 C++ 런타임 시스템이 이 역할을 맡아야 한다.
  • 이 상황에서 C++ 런타임 시스템이 하는 일은?
    • 호출한 operator new 함수와 짝이 되는 operator delete를 호출하여 메모리를 해제한다.
  • 결국 operator new와 짝이 맞는 operator delete가 중요한데..
  • 기본형 new / delete에서는 어차피 이미 마련되어 있는 형태들이 있으므로 문제가 되지 않는다.
// 기본형 operator new
void* operator new(std::size_t) throw(std::bad_alloc);

// 기본형 operator delete
void operator delete(void* rawMemory) throw(); // 전역 유효범위용
void operator delete(void* rawMemory, std::size_t size) throw(); // 클래스 유효범위용
  • 즉, 문제가 되는 것은 기본형이 아닌 형태의 operator new의 경우이다.
    • 비기본형이란 다른 매개변수를 추가로 가지는 operator new를 뜻한다.
    • 이 경우, 이 new에 어떤 delete를 짝맞춰야 하는지 C++이 알지 못한다!
class Widget
{
public:
    ...
    // 비기본형 operator new 형태
    static void* operator new(std::size_t size,
                              std::ostream& logStream) throw(std::bad_alloc);
    
    // 기본형 operator delete 형태
    static void operator delete(void* pMemory,
                                size_t size) throw();
    ...
};

 

 

 

위치지정 new(placement new)
  • 추가 매개변수를 받는 new를 뜻한다.
  • 위치지정 new 중 특히 유용한 경우가 있는데,
    • 생성한 객체를 할당할 포인터를 매개변수로 받는 위치지정 new

 

 

 

생성한 객체를 할당할 포인터를 매개변수로 받는 위치지정 new
// pMemory 매개변수는 operator new가 생성한 객체를 할당할 목표 포인터이다.
void* operator new(std::size_t, void* pMemory) throw();
  • 이 위치지정 new는 C++ 표준 라이브러리에 포함되어 있기까지 하다.( #include <new> )
  • vector도 이 위치지정 new를 사용한다.
    • 해당 벡터의 미사용 공간에 원소 객체를 생성하고자 할 때.
  • 위치지정 new라는 용어 자체도 이 경우에서 시작되었기 때문에 바로 그런 이름이 된 것이다.

 

 

 

Widget 클래스 설계의 문제점
  • 위 예시의 Widget 클래스는 메모리 누출을 유발할 수 있다.
// 위치지정 new를 호출한다.
Widget* pw = new (std::cerr) Widget;

// Widget 생성자에서 예외가 발생하면,
// 위치지정 delete를 찾을 수 없기 때문에 메모리가 누출된다!
  • 런타임 시스템 쪽에서는 호출된 operator new가 어떻게 동작하는지 알아낼 방법이 없다.
    • 그러므로 그저 짝이 되는 operator delete를 호출할 뿐이다.
  • 짝이 되는 operator delete란?
    • 매개변수의 개수 및 타입이 똑같은 버전의 operator delete.(즉, 위치지정 delete)
  • 결국 이 경우, 짝이 되는 위치지정 delete가 없으므로 어떤 operator delete도 호출하지 않는다!!
  • 즉 위치지정 new를 만들었다면 짝이 되는 위치지정 delete를 만들어줘야 한다.
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();
    ...
};

 

 

 

위치지정 delete와 기본형 delete
  • 만일 생성자에서 예외가 발생하지 않았고, 사용자 코드의 delete 문에 다다르면 어떻게 될까?
Widget* pw = new (std::cerr) Widget; // 성공!
...
delete pw; // 이 경우 어떻게 될까?
  • 이 상황에서는 위치지정 delete가 아닌 기본형 delete가 호출된다!!
  • 위치지정 delete가 호출되는 경우는,
    • 위치지정 new가 호출되고 난 후의 생성자에서 예외가 발생할 때 뿐이다.
    • 즉, 위 상황에서는 절대로 위치지정 delete가 불리지 않는다는 뜻이다.

 

 

 

 결론
  • 위치지정 new를 만들었다면?
  • 기본형 operator delete, 위치지정 delete 두 가지 버전이 모두 마련되어 있어야 한다.

 

 

 

유의 사항
  • 바깥 유효범위 함수와 클래스 멤버 함수는 이름이 같으면, 그저 이름만 같아도 가려진다.(항목 33 참조)
// 전역 함수의 이름 가리기 문제
class Base
{
public:
    ...
    // 이 new가 표준 형태의 전역 new를 가려버린다.
    static void* operator new(std::size_t size,
                              std::ostream& logStream) throw(std::bad_alloc);
    ...
};

Base* pb = new Base; // 컴파일 에러!! 전역 operator new가 가려져있음.
Base* pb = new (std::cerr) Base; // 성공. 위치지정 new 호출.

// 상속에서도 이름 가리기 문제는 발생한다.
class Derived : public Base
{
public:
    ...
    // 기본형 new를 클래스 전용으로 재선언한다.
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ...
};

Derived* pd = new (std::clog) Derived; // 컴파일 에러!! Base의 위치지정 new가 가려져있음.
Derived* pd = new Derived; // 성공. Derived의 operator new 호출.
  • 기본적으로 C++가 전역 유효범위에서 제공하는 operator new의 형태는 아래와 같다. (항목 49 참조)
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(항목 49 참조)
  • 해결법
    • 그냥 클래스 전용 버전이 전역 버전을 호출하도록 구현하면 된다.
    • 하지만 역시 번거롭다.
  • 번거로움을 제거한 해결법
    • 기본 클래스를 만들고, 이 안에 new 및 delete의 기본 형태를 모두 넣어둔다.
    • 그리고 위치지정 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(size, 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&) throw()
    {
        ::operator delete(pMemory);
    }
};

// 상속으로 전역 new / delete를 사용할 수 있도록 한다.
class Widget : public StandardNewDeleteForms
{
public:
    // 표준 형태가 Widget 내부에 보이도록 만든다.
    using StandardNewDeleteForms::operator new;
    using StandardNewDeleteForm::operator delete;
    
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
    static void operator delete(void* pMemory, std::ostream& logStream) throw();
    ...
};
728x90
Comments