[Effective C++ 3판] Chapter 8. new와 delete를 내 맘대로 (항목 49 ~ 52)

Chapter 8. new와 delete를 내 맘대로 (항목 49 ~ 52)

항목 49. new 처리자의 동작 원리를 제대로 이해하자.

[new와 delete 키워드]
new/new[] 키워드 : (알맞는 operator new 호출 -> 생성자 호출) + 알파 (operator new와 생성자를 적절하게 호출하고 메타데이터등을 저장하고 관리하는 코드. 여기서 메타데이터는 new[]할때 객체의 갯수등을 의미.)
delete/delete[] 키워드 : 위와 비슷한 맥락.

[new 처리자 - new handler]
operator new의 역할 : 필요한 메모리를 적절하게 할당해서 반환해주는 역할.
new 처리자 : operator new가 메모리를 할당하는데 실패했을 때 호출되는 함수.

[new 처리자가 해야하는 동작 (아래중 하나를 해야한다.)]
- 사용할 수 있는 메모리를 더 많이 확보한다.
- 다른 new 처리자를 설치한다.
- new 처리자의 설치를 제거한다.
- 예외를 던진다.
- 복귀하지 않는다. (abort나 exit을 호출)
#include <new>

// RAII방식으로 new handler을 관리하는 클래스
class NewHandlerRAII {
public:
    explicit NewHandlerRAII(std::new_handler nh) noexcept {
        old = std::set_new_handler(nh);
    }
    ~NewHandlerRAII() noexcept {
        std::set_new_handler(old);
    }
    NewHandlerRAII(const NewHandlerRAII&) = delete;
    NewHandlerRAII& operator=(const NewHandlerRAII&) = delete;
private:
    std::new_handler old;
};

// class별로 new handler를 지정할 수 있는 기능과 인터페이스를 담고 있는 클래스 템플릿.
template <class T>
class NewHandlerFeature {
public:
    static std::new_handler set_new_handler(std::new_handler nh) noexcept {
        auto old = currentHandler;
        currentHandler = nh;
        return old;
    }
    static void* operator new(std::size_t size) throw(std::bad_alloc) {
        NewHandlerRAII nhr(currentHandler);
        return ::operator new(size);
    }
    static void* operator new(std::size_t size, void *pMem) noexcept {
        // NewHandlerRAII nhr(currentHandler);
        return ::operator new(size, pMem);
    }
    static void* operator new(std::size_t size, const std::nothrow_t& nt) noexcept {
        NewHandlerRAII nhr(currentHandler);
        return ::operator new(size, nt);
    }
    // operator new[]는 생략..
private:
    static std::new_handler currentHandler;
};
template <class T>
std::new_handler NewHandlerFeature<T>::currentHandler = nullptr;

// 신기하게 반복되는 템플릿 패턴 (CRTP : Curiously Recurring Template Pattern)
// also called to "나만의 것(Do It For Me)" by Scott Meyers.
// CRTP패턴을 사용함으로써 기본 클래스의 정적변수를 파생 클래스별로 각자 소유할 수 있게된다.
class Example : public NewHandlerFeature<Example> {
public:
    Example() {}
    ~Example() {}
private:
    char dummy[8];
};

void func() {}

int main() {
    Example::set_new_handler(func);
    auto ptr1 = new Example();
    char buf[sizeof(Example)];
    auto ptr2 = new (buf) Example();
    auto ptr3 = new (std::nothrow) Example();
}

[이것만은 잊지 말자]
- set_new_handler 함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출되는 함수를 지정할 수 있습니다.
- 예외불가(nothrow) new는 영향력이 제한되어 있습니다. 메모리 할당 자체에만 적용되기 때문입니다. 이후에 호출되는 생성자에서는 얼마든지 예외를 던질 수 있습니다.

항목 50. new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자.

일단은 기본 제공 new/delete를 사용하고, 추후 프로그램 프로파일링등을 통해 얻은 정보를 바탕으로 new/delete를 적절히 교체한다. 직접 구현하려면 신경쓸 부분이 많으므로 우선적으로 오픈소스/상용 메모리 관리 함수를 찾아본다.

[new/delete를 바꿔야 할 때]
- 잘못된 힙 사용을 탐지하기 위해
- 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 할당 및 해제 속력을 높이기 위해
- 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
- 적당히 타협한 기본 할당자의 바이트 정렬 동작을 보정하기 위해
- 임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아 놓기 위해 (locality를 극대화하고 싶을 때..별로 힙생성등을 통해 관리)
- 그때그때 원하는 동작을 수행하도록 하기 위해 (메모리 할당/해제를 공유메모리로 부터 하고싶을 때 등등)

[이것만은 잊지 말자]
- 개발자가 스스로 사용자 정의 new 및 delete를 작성하는 데는 여러 가지 나름대로 타당한 이유가 있습니다. 여기에는 수행 성능을 향상시키려는 목적, 힙 사용 시의 에러를 디버깅하려는 목적, 힙 사용 정보를 수집하려는 목적 등이 포함됩니다.

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

#include <new>

class Example {
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc) {

        if (size == 0) {  // 0 byte request.
            size = 1;
        }
        else if (size != sizeof(Example)) {  // new request of Derived Object.
            return ::operator new(size);
        }

        void *pMem = malloc(size);
        while (pMem == nullptr) {
            std::new_handler nh = std::set_new_handler(nullptr);
            std::set_new_handler(nh);

            if (nh) {
                nh();
            }
            else {
                throw std::bad_alloc();
            }

            pMem = malloc(size);
        }

        return pMem;
    }

    static void operator delete(void *pMem) noexcept {
        if (pMem == nullptr) {  // NULL pointer.
            return;
        }
        free(pMem);
    }
};

int main() {
    auto ptr = new Example();
    delete ptr;
}

[이것만은 잊지 말자]
- 관례적으로, operator new함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0바이트에 대한 대책도 있어야 합니다. 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰(다른) 메모리 블록에 대한 요구도 처리해야 합니다.
- operator delete 함수는 널 포인터가 들어왔을 때 아무 일도 하지 않아야 합니다. 클래스 전용 버전의 경우에는 예정 크기보다 더 큰 블록을 처리해야 합니다.

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

// 항목33처럼 이름 가려짐 문제가 발생할 수 있다.
// 기본클래스 내부의 이름이 가려지는 건 using으로 해결할 수 있지만, 전역 이름의 가려짐은 해결할 수 없다.
// 따라서 전역 operator new/delete의 wrapper interface를 만듬으로서 해결한다.
// 좀 더 편하게 하려면 wrapper interface들을 가지는 클래스를 하나 만들고 이걸 상속함으로서 해결한다.
namespace name_hiding {
    namespace problem {
        class Example {
        public:
            using ::operator new;  // compile error. it is impossible :(
            static void* operator new(std::size_t size, void *pMem) noexcept {
                ...
            }
        };
        void situation() {
            new Example();  // compile error.
        }
    }
    namespace solution {
        class GlobalNewDeleteFeature { ... };  // it has wrapper interfaces to global operator new/delete
        class Example : public GlobalNewDeleteFeature {
        public:
            using GlobalNewDeleteFeature::operator new;
            using GlobalNewDeleteFeature::operator delete;
            static void* operator new(std::size_t size, void *pMem) noexcept {
                ...
            }
        };
        void situation() {
            new Example();  // compile ok.
        }
    }
}

// new 키워드로 객체를 생성할 때, 생성자에서 예외가 발생할 경우, operator delete가 호출되어야만 한다.
// 메모리할당시 사용된 operator new와 "추가 매개변수" 개수/타입이 똑같은 operator delete을 찾아서 호출한다.
// 만약, 그러한 operator delete가 존재하지 않는다면 아무것도 호출되지않고, memory leak이 발생하게 된다.
// 해결책은 항상 operator new와 operator delete는 짝이 맞게 정의하는 것이다.
// 추가적으로 new키워드와 delete키워드 또한 항상 짝이 맞게 사용해주어야한다. 그렇지 않을경우 문제가 생길 수 있다.
namespace memory_leak {
    class Example {
    public:
        Example() {
            throw 1;
        }
        static void* operator new(std::size_t size, void *pMem) noexcept {
            return ::operator new(size, pMem);
        }
    };
    void situation() {
        char buf[sizeof(Example)];
        auto ptr = new (buf) Example(); // operator delete is not called.
        delete ptr;  // call basic operator delete(="void ::operator delete(void*)").
    }
}

[이것만은 잊지 말자]
- operator new 함수의 위치지정 버전을 만들 대는, 이 함수와 짝을 이루는 위치지정 버전의 operator delete 함수도 꼭 만들어 주세요. 이 일을 빼먹었다가는, 찾아내기도 힘들며 또 생겼다가 안 생겼다 하는 메모리 누출 현상을 경험하게 됩니다.
- new 및 delete의 위치지정 버전을 선언할 때는, 의도한 바도 아닌데 이들의 표준 버전이 가려지는 일이 생기지 않도록 주의해 주세요.



빨리 new/delete를 죄다 커스터마이징해버리고 충동이 드는군요!! 여러분들도 같은 마음일꺼라 생각합니다!

댓글