Skip to main content

[Effective C++] Chapter 8. new와 delete를 내 맘대로

이펙티브 C++ 책을 읽고 공부한 노트입니다.




Item 49: new 처리자의 동작 원리를 제대로 이해하자 #

new 처리자(new handler) #

  • new메모리 할당이 제대로 되지 못하면 호출되는 에러 처리 함수이다.
  • 사용자가 직접 지정할 수 있으며, 표준 라이브러리에 있는 set_new_handler 함수를 이용하면 된다.
// <new>에 선언되어 있는 함수들

namespace std
{
  typedef void (*new_handler)();
  // 매개변수와 반환값이 없는 함수에 대한 포인터이다. 
  
  new_handler set_new_handler(new_handler p) throw();
  // 매개변수 : 새롭게 설치할 new 처리자 
  // 반환값 : 새롭게 설치하기 전까지 사용하던 new 처리자 
  // throw() : 이 함수는 어떤 예외도 던지지 않을 것이라는 뜻이다. 
}

  • new 처리자는 이것들 중 하나는 꼭 처리해야 한다.
    • 사용할 수 있는 메모리를 더 많이 확보한다.
    • 다른 new 처리자를 설치한다.
    • new 처리자의 설치를 제거한다.
    • bad_alloc 혹은 그것에서 파생된 타입의 예외를 던진다.
    • 복귀하지 않는다. (대개 abort 혹은 exit을 호출한다.)



new 처리자를 클래스 타입에 따라 다르게 만들기 #

// 자원 관리 객체를 통해 할당에러를 처리한다. 
class NewHandlerHolder
{
private:
  std::new_handler handler;
  
  // 복사를 막기 위함 (Item 14)
  NewHandlerHolder(const NewHandlerHolder&);
  NewHandlerHolder& operator=(const NewHandlerHolder&);
  
public:
  // 매개변수로 들어온 처리자를 들고 있다가  
  explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {}
  
  // 객체가 소멸될 때 그 처리자를 설치한다. 
  ~NewHandlerHolder() { std::set_new_handler(handler); }
};

class Widget
{
private:
  static std::new_handler currentHandler;
  
public:
  static std::new_handler set_new_handler(std::new_handler p) throw();
  static void * operator new(std::size_t size) throw(std::bad_alloc);
};
std::new_handler Widget::currentHandler = 0; // 널로 초기화

std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
  // 매개변수로 들어온 처리자를 들고 있게 한다. 
  std::new_handler oldHandler = currentHandler;
  currentHandler = p;
  return oldHandler;
}

void * Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
  // 가지고 있는 현재 처리자를 설치하고, 
  // 이전 처리자는 NewHandlerHolder가 가지고 있다가
  // 이 함수를 빠져나가면 이전 처리자(전역 처리자)로 재설치한다. 
  NewHandlerHolder h(std::set_new_handler(currentHandler));
  
  // 메모리를 할당하고, 실패하면 예외를 던진다. 
  return ::operator new(size); 
}
void outOfMem();

int main()
{
  // 새로운 처리자 설치 
  Widget::set_new_handler(outOfMem);
  
  // 여기서 new 실패시 outOfMem이 불린다. 
  Widget * pw1 = new Widget;
  
  // 여기서 new 실패시 전역 new 처리자가 불린다. 
  std::string * ps = new std::string;
  
  // null로 설정 
  Widget::set_new_handler(0);
  
  // 여기서 new 실패시 예외를 바로 던진다. 
  Widget * pw2 = new Widget;
}



  • NewHandlerHolder를 템플릿으로 바꿔서 다른 클래스에서도 재사용 할 수 있게 해보자.
template<typename T>
class NewHandlerSupport
{
private:
  static std::new_handler currentHandler;
  
public:
  static std::new_handler set_new_handler(std::new_hanlder p) throw();
  static void * operator new(std::size_t size) throw(std::bad_alloc);
};

template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_hanlder p) throw()
{
  std::new_handler oldHandler = currentHandler;
  currentHandler = p;
  return oldHandler;
}

template<typename T>
void * newHandlerSupport<T>::operator new(std::size_t size) throw(bad_alloc)
{
  NewHandlerSupport h(std::set_new_handler(currentHandler));
  return ::operator new(size);
}

template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
// 신기하게 반복되는 템플릿 패턴(curiously recurring template pattern: CRTP)
// Widget이 필요한건, NewHandlerSupport에 대한 사본이다. 
// NewHandlerSupport은 마치, 인스턴스화될 때 전달되는 T를 위한 
// NewHandlerSupport의 사본을 찍어내는 공장과 같다. 
class Widget : public NewHandlerSupport<Widget>
{
  // ...
};



예외불가(nothrow) new #

  • 현재는 new 실패 시 bad_alloc 예외를 던지도록 되어 있다.
  • 예외불가 new는 예전의 방식대로, 실패 시 null을 반환하는 것이다.
Widget * pw1 = new Widget;
// Widget을 할당하다가 실패하면 bad_alloc 예외를 던진다. 

if (pw1 == 0)
// 이것은 늘 false일 것이다. 

Widget * pw2 = new (std::nothrow) Widget;
// Widget을 할당하다가 실패하면 0(널)을 반환한다. 

if (pw2 == 0)
// 실패하면 널을 반환하므로 점검해보는 것이 가능하다. 

  • 참고로 예외불가 newoperator new에서만 예외가 발생하지 않도록 보장하는 것일 뿐, Widget의 생성자에서 예외가 나오지 않게 막아 준다는 것은 아니다.



Item 50: newdelete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자 #

  • 사용자 정의 newdelete를 작성해야 하는 때란?
    • 잘못된 힙 사용을 탐지하기 위해
    • 효율을 향상시키기 위해
    • 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
    • 할당 및 해제 속력을 높이기 위해
    • 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
    • 적당히 타협한 기본 할당자의 바이트 정렬 동작을 보장하기 위해
    • 임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해
    • 그때그때 원하는 동작을 수행하도록 하기 위해

  • 바이트 정렬(byte alignment) 문제
    • 컴퓨터는 아키텍처 적으로 특정 타입의 데이터가 특정 종류의 메모리 주소를 시작 주소로 하여 저장될 것을 요구사항으로 두고 있다.
    • 예를 들면, 포인터는 4바이트 단위로 정렬되어야 한다거나 double은 8바이트 단위로 정렬되어야 한다는 것이다.
    • 모든 operator new 함수는 어떤 데이터 타입에도 바이트 정렬을 적절히 만족하는 포인터를 반환해야 한다.
    • 이런 바이트 정렬을 어떻게 다루느냐에 따라 사용자 정의 버전을 제공하는 메모리 관리자의 품질이 달라질 것이다.



Item 51: newdelete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 #

operator new를 구현할 때 지켜야할 요구사항들 #

  • (1) 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 한다.
  • (2) 할당 실패 시 new 처리자 함수를 호출해야 한다.
  • (3) 크기가 없는(0바이트) 메모리 요청에 대한 대비책을 마련해야 한다.
  • (4) 기본 형태의 new가 가려지지 않도록 해야한다.

void * operator new(std::size_t size) throw(std::bad_alloc)
{
  using namespace std;
  
  // (3) 0바이트 메모리 요청이 들어오면 1바이트 요구로 간주한다. 
  if (size == 0) size = 1;
  
  // (1) opreator new 함수에는 무한 루프가 있다. 
  // 그래서 메모리 할당에 성공하든지, 
  // 아니면 Item 49에서 나왔던 것들 중 하나는 꼭 처리해야 한다. 
  while(true)
  {
    size바이트를 할당해 본다. 
    if (할당 성공) return 할당한 메모리의 포인터;
    
    // 현재 설정되어 있는 new 처리자를 얻는다. 
    new_handler globalHandler = set_new_handler(0);
    set_new_hanlder(globalHandler);
    
    // (2) new 처리자를 호출한다. 
    if (globalHanlder) (*globalHandler)();
    else               throw std::bad_alloc();
  }
}

  • 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰(다른) 메모리 블록에 대한 요구도 처리해야 한다.
    • 클래스 X를 위한 operator newsizeof(X)인 크기의 객체에 맞추어져 있다.
    • 그래서 Base 클래스에서 선언된 operator newDerived가 호출하면 매개변수로 넘어오는 size가 달라지게 된다. (BaseDerived의 크기는 다르다. )
    • 따라서 다음과 같은 처리가 필요할 것이다.
void * Base::operator new(std::size_t size) throw(bad_alloc)
{
  if (size != sizeof(Base))
    return ::operator new(size);
  // size가 Base와 다르면 표준 operator new 쪽에서 처리하도록 넘긴다. 
}



operator delete를 구현할 때 지켜야할 요구사항들 #

  • 널 포인터에 대한 delete 적용이 항상 안전하도록 보장해야 한다.
  • 클래스 전용 버전의 경우에는 new와 똑같이 크기를 점검하는 코드를 넣어준다.
void Base::operator delete(void * rawMemory, std::size_t size) throw()
{
  // 널 포인터의 경우 아무것도 하지 않는다. 
  if (rawMemory == 0) return;

  // 클래스 전용 버전인 경우 size를 점검한다. 
  if (size != sizeof(Base))
  {
    ::operator delete(rawMemory); // 표준 operator delete가 처리하도록 한다. 
    return;
  }
  
  // ... rawMemory가 가리키는 메모리를 해제한다. 
}

  • 추가적으로, 가상 소멸자가 없는 기본 클래스로부터 파생된 클래스의 객체를 삭제하려고 할 경우에는, C++이 operator delete로 넘기는 size_t 값이 엉터리일 수 있다.
    • 따라서 기본 클래스는 반드시 가상 소멸자를 두어야 하겠다.



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

C++가 전역 유효범위에서 제공하는 operator new의 세 가지 표준 형태 #

// 기본형 new
void * operator new(std::size_t) throw(std::bad_alloc);

// 위치지정 new
void * operator new(std::size_t, void *) throw();

// 예외불가 new (Item 49)
void * operator new(std::size_t, const std::nothrow_t&) throw();



위치지정(placement) new #

  • 추가적인 매개변수를 받는 operator new이다.
  • 이름이 위치지정인 이유는 원조격인 위치지정 new가 추가적인 매개변수로, 할당할 메모리의 위치를 받아서 할당 위치를 정해주었기 때문이다.

  • 런타임 시스템은 operator new가 받아들이는 매개변수의 개수 및 타입이 똑같은 버전의 operator delete를 찾고, 그 녀석을 호출한다.
    • 따라서 위치지정 new를 만들었다면, 그와 똑같은 매개변수를 받아들이는 위치지정 delete를 만들어야 한다.
    • 위치지정 delete가 호출되는 때는, 위치지정 new와 함께 호출된 생성자에서 예외가 발생했을 때뿐이다.
class Widget
{
public:
  // 새로운 매개변수 ostream을 받는 operator new (비표준 형태)
  static void * operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
  
  // 클래스 전용 operator delete (표준 형태)
  static void operator delete(void * rawMemory, std::size_t size) throw();
};

int main()
{
  Widget * pw = new (std::cerr) Widget;
  // 여기서 Widget생성자에서 오류가 난다면?
  // 그럼 delete가 제대로 되겠는가?
  // 새로운 매개변수 ostream을 받는 new와 짝이 맞는 delete가 없어서
  // 결국 아무것도 불리지 않고 메모리는 누수가 된다. 
  // 따라서 새로운 매개변수 ostream을 받는 delete도 있어야 하겠다. 
}

  • 함수는 이름만 같아도 가려지게 되어있다(Item 33). 따라서 새로 정의한 위치지정 newdelete 때문에 표준버전들이 가리지 않도록 주의해야한다.
    • 좋은 방법은, 기본 클래스 하나를 만들고, 이 안에 newdelete의 기본 형태를 전부 넣어두는 것이다.
    • 그 다음 상속과 using 선언을 사용해서 표준 형태를 파생 클래스로 끌어오고, 원하는 사용자 정의 형태를 선언하는 것이다.
class StandardNewDeleteForms
{
public:
  // 기본형 
  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); }
  
  // 위치지정 
  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(pMemory, ptr); }
  
  // 예외불가 
  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); }
};

class Widget : public StandardNewDeleteForms
{
public:
  // StandardNewDeleteForms의 표준 형태가 Widget 내부에서 보이도록 만든다. 
  using StandardNewDeleteForms::operator new;
  using StandardNewDeleteForms::operator delete;
 
  // 새롭게 정의한 위치지정 new와 그와 짝이 맞는 위치지정 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();
};