스토리텔링 개발자

[Effective C++] 51. operator new / delete 커스텀 관례 본문

개발/Effective C++

[Effective C++] 51. operator new / delete 커스텀 관례

김디트 2024. 7. 25. 11:23
728x90

항목 51. new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아두자

 

 

 

operator new 구현 시 요구사항
  1. 반환값이 제대로 되어 있어야 한다.
  2. 가용 메모리가 부족할 경우 new 처리자 함수를 호출해야 한다. (항목 49 참조)
  3. 크기가 없는(0바이트) 메모리 요청에 대한 대비를 해야 한다.
  4. 실수로 ‘기본(normal)’ 형태의 new가 가려지지 않도록 해야 한다. (항목 52 참조)

 

 

 

요구사항을 지키며 operator new 구현
  • 구현?
    • 요청된 메모리를 마련해 줄 수 있는 경우
      • 그 메모리에 대한 포인터를 반환한다.
    • 요청된 메모리를 마련해 줄 수 없는 경우
      • bad_alloc 타입의 예외를 던지면 된다. (항목 49 참조)
    • 말로는 쉽지만 실제 구현은 꽤 까다롭다..
  •  문제점
    • operator new는 메모리 할당이 실패할 때마다 new 처리자 함수를 호출한다.
      • 그러면서 메모리 할당을 2회 이상 시도하게 된다.(operator new 내부에는 무한 루프가 있기 때문이다.)
      • 즉, 할당 실패한 메모리에 대한 해제를 new 처리자 함수 쪽에서 맡을 것으로 가정한다.
      • operator new가 예외를 던지는 상황은 오직 new 처리자 함수에 대한 포인터가 null일 때 뿐이다.
    • 0바이트가 요구되었을 때조차도 operator new 함수는 적법한 포인터를 반환해야 한다.
  • 의사 코드
void* operator new(std::size_t size) throw(std::bad_alloc)
{
    using namespace std;
    
    // 0바이트를 요구한 경우
    if(size == 0)
    {
        // 1바이트를 요구한 것으로 간주하고 처리한다.
        size = 1;
    }
    
    while(true)
    {
        size 바이트를 할당;
        
        if(할당 성공한 경우)
            return (할당된 메모리에 대한 포인터);
            
        // 할당이 실패했을 경우, 현재의 new 처리자 함수가
        // 어느 것으로 설정되어 있는지 찾아내어야 한다.
        // globalHandler를 가져와서 호출해주기 위한 null 설정 -> 재설정
        new_handler globalHandler = set_new_handler(0);
        set_new_handler(globalHandler);
        
        if(globalHandler) (*globalHandler)();
        else throw std::bad_alloc();
    }
}
  • 사용된 트릭
    1. 외부에서 0바이트를 요구했을 때 1바이트 요구인 것으로 간주하고 처리한다.
    2. new 처리자 함수를 가져오기 위해 널로 설정하고 바로 뒤에서 되돌려 놓는다.
      • 안타깝지만 현재의 전역 new 처리자 함수를 얻어오는 직접적인 방법이 없다?
      • 모던 C++에서는 std::get_new_handler를 지원한다.
    3. operator new 함수에는 무한 루프가 들어있다.
      • 무한루프를 빠져나오는 조건
        1. 메모리 할당이 성공한다
        2. 아래 동작 중 하나를 new 처리자 함수가 처리해준다.(항목 49 참조)
          • 가용 메모리를 늘린다.
          • 다른 new 처리자를 설치한다.
          • bad_alloc 혹은 그에 파생된 타입의 예외를 던진다.
          • 함수 복귀를 포기하고 프로그램을 중단시킨다.
      • 즉, new 처리자 함수가 네 가지 동작 중 하나를 하지 않으면 무한 루프에 빠질 것이다!

 

 

 

operator new는 상속이 되는 함수라는 점을 유의할 것.
  • 특정 클래스 전용 할당자를 만들어 효율을 최적화한 경우,
  • 해당 특정 클래스를 상속받은 파생 클래스에서 해당 할당자를 호출하게 되는 상황이 벌어질 수 있다.
  • 이렇게 되면 파생 클래스의 객체를 할당하지 않고 기본 클래스 객체가 호출되는 문제가 발생한다.
class Base
{
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ...
};

// Derived에서는 operator new가 선언되지 않았다.
class Derived : public Base
{ ... };

// Base::operator new가 호출되는 문제 발생!!
Derived* p = new Derived;
  • 위 경우에 대한 보완 처리
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
    // 잘못된 크기가 들어오면 표준 operator new 호출
    if(size != sizeof(Base))
        return ::operator new(size);
    
    ...
};
  • 이 경우, 해당 체크에 0바이트 점검이 함께 처리된다.
    • C++에는 모든 독립 구조(freestanding) 객체는 반드시 크기가 0이 넘어야 한다는 금기사항이 있기 때문이다.(항목 39 참조)
    • 그러므로 sizeof(Base)가 0이 될 일은 절대 없다.
    • 따라서 size가 0이면 if문이 거짓이 되며 표준 operator new 쪽으로 넘어가므로 처리를 제대로 한 것이 된다.

 

 

 

배열 메모리 할당 구현하기
  • operator new[] 함수(배열 new(array new))를 구현하면 된다.
  • operator new[] 안에서 해줄 일은 단순히 원시 메모리의 덩어리를 할당하는 것 밖엔 없다.
  • 원시 메모리 덩어리 할당만 가능한 이유
    1. 이 시점에서는 배열 메모리에 아직 생기지도 않은 클래스 객체에 대해 아무 것도 할 수 없다.
    2. 배열 안에 몇 개의 객체가 들어갈지 계산조차 불가능하다.
      1. 객체 하나가 얼마나 클지 확정할 방법이 없기 때문이다.
        • 파생 클래스 객체에서도 operator new[] 가 호출될 수 있다는 점 때문이다.
        • 그러므로 Base::operator new[] 안에서조차 배열 객체 하나의 크기가 sizeof(Base)라는 가정을 할 수 없다.
        • 즉, 객체의 갯수를 (요구된 바이트 수 / sizeof(Base)) 로 계산할 수 없다는 뜻이다.
      2. operator new[]에 넘어가는 size_t 타입의 인자는 객체들을 담기에 딱 맞는 메모리 양보다 더 많게 설정되어 있을 수도 있다.
        • 동적으로 할당된 배열에는 배열 원소의 개수를 담기 위한 자투리 공간이 추가로 들어간다. (항목 16 참조)

 

 

 

operator delete 구현 시 요구사항
  • operator new의 경우보다 더 간단하다.
  • C++는 null 포인터에 대한 delete 요청에 대해 항상 안전하도록 보장하기만 하면 된다.
void operator delete(void* rawMemory) throw()
{
    // null 포인터가 delete 되려 할 때는 아무것도 하지 않는다.
    if(rawMemory == 0) return;
    
    rawMemory가 가리키는 메모리를 해제한다;
}
  • 클래스 전용 버전 operator delete에는 삭제될 메모리의 크기를 점검하는 코드를 넣어주어야 한다.
class Base
{
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    static void operator delete(void* rawMemory, std::size_t size) throw();
    ...
};

void Base::operator delete(void* rawMemory, std::size_t size) throw()
{
    // null 포인터 점검
    if(rawMemory == 0) return;
    
    // 크기가 틀린 경우 표준 operator delete 호출
    if(size != sizeof(base))
    {
        ::operator delete(rawMemory);
        return;
    }
    
    rawMemory가 가리키는 메모리를 해제한다;
    
    return;
}

 

 

 

번외
  • 가상 소멸자가 없는 기본클래스로부터의 파생클래스 객체를 삭제하려 할 경우
  • operator delete로 C++가 넘기는 size_t 값이 엉터리일 수 있다.(미정의 동작!)
  • 그러니까 기본 클래스는 가상 소멸자를 꼭 두도록 하자. (항목 7 참조)
728x90
Comments