이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 350p ~ 361p
이펙티브 C++(350p ~ 361p)
요약
항목 49: new 처리자의 동작 원리를 제대로 이해하자
- new를 사용할 때 할당된 객체의 클래스 타입에 따라서 메모리 할당 실패에 대한 처리를 다르게 가져가고 싶은 경우가 있다.
class X {
public:
static void outOfMemory();
};
class Y {
public:
static void outOfMemory();
};
X* p1 = new X; // 메모리 할당이 실패하면 X::outOfMemory 호출
Y* p2 = new Y; // 메모리 할당이 실패하면 Y::outOfMemory 호출
- C++에는 특정 클래스만을 위한 할당에러 처리자를 둘 수 있는 기능은 없다.
- 하지만 클래스 내부에서 자체 버전의
set_new_handler
및operator new
를 제공하도록 만들어 주기만 하면 된다. - 클래스에서 제공하는
set_new_handler
함수의 역할은 사용자로부터 그 클래스에 쓰기 위한 new 처리자를 받아내는 것이다. - 클래스에서 제공하는
operator new
함수는 그 클래스 객체를 담을 메모리가 할당되려고 할 때(그리고 실패했을 때) 전역 new 처리자 대신 클래스 버전의 new 처리자가 호출되도록 만드는 역할을 한다. - 다음은
Widget
클래스에 대한 메모리 할당 실패를 직접 처리하고 싶은 경우의 예시이다.Widget
객체를 담을 만큼의 메모리를operator new
함수가 할당하지 못할 경우에 호출될 new 처리자 함수를new_handler
타입의 정적 멤버 데이터로 선언한다.
class Widget {
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);
private:
static std::new_handler currentHandler;
};
- 정적 클래스 멤버의 정의는 그 클래스 바깥쪽에 있어야 하므로 다음과 같이 하면 된다.
std::new_handler Widget::currentHandler = 0;
Widget
이 제공하는set_new_handler
함수는 자신에게 넘어온 포인터를 아무런 점검 없이 저장해 놓고, 바로 전에 저장했던 포인터를 역시 아무런 점검 없이 반환하는 역할만 맡는다. 표준 라이브러리의set_new_handler
가 담당하는 일과 같다.
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
- 이제 마지막으로
Widget
의operator new
함수가 할 일만 남았다.- 표준
set_new_handler
함수에Widget
의 new 처리자를 넘겨서 호출한다. 즉, 전역 new 처리자로서Widget
의 new 처리자를 설치한다. - 전역
operator new
를 호출하여 실제 메모리 할당을 수행한다. 전역operator new
의 할당이 실패하면, 이 함수는Widget
의 new 처리자를 호출하게 된다. 바로 앞 단계에서 전역 new 처리자로 설치된 함수가 바로 이 함수이다. 마지막까지 전역operator new
의 메모리 할당 시도가 실패하면, 전역operator new
는 좌절을 선언하는 의미로bad_alloc
을 던진다.- 이 경우
Widget
의operator new
는 전역 new 처리자를 원래의 것으로 되돌려 놓고, 이 예외를 전파시켜야 한다. - 원래의 전역 new 처리자를 항상 실수 없이 되돌려놓을 수 있도록,
Widget
은 전역 new 처리자를 자원으로 간주하고 처리한다.(자원 관리 객체를 사용하여 전역 new 처리자를 관리함으로써 자원 누수를 막는다.)
- 이 경우
- 전역
operator new
함수가Widget
객체 하나만큼의 메모리를 할당할 수 있으면,Widget
의operator new
는 이렇게 할당된 메모리를 반환한다. 이와 동시에, 전역 new 처리자를 관리하는 객체의 소멸자가 호출되면서Widget
의operator new
가 호출되기 전에 쓰이고 있던 전역 new 처리자가 자동으로 복원된다.
- 표준
- 단계적으로 구현할 차례이다. 전역 new 처리자를 자원으로 삼는다고 했으므로, 우선 자원 관리 클래스를 하나 준비한다. RAII 연산 외엔 아무것도 없는 단순한 클래스이다.
class NewHandlerHolder {
public:
// 현재의 new 처리자를 획득
explicit NewHandlerHolder(std::new_handler nh)
:handler(nh) {}
// 이것을 해제
~NewHandlerHolder() { std::set_new_handler(handler); }
private:
// 이것을 기억
std::new_handler handler;
// 복사를 막기 위한 부분
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
Widget
의operator new
는 다음과 같이 구현한다.
void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
// Widget의 new 처리자를 설치한다.
// 현재의 new 처리자가 반환되어 NewHandlerHolder에 저장된다.
NewHandlerHolder h(std::set_new_handler(currentHandler));
// 메모리를 할당하거나 할당이 실패하면 예외를 던진다.
return ::operator new(size);
} // 이전의 전역 new 처리자가 자동으로 복원된다.
// NewHandlerHolder에서 현재의 new 처리자를 복원한다.
Widget
클래스를 사용하는 쪽에서 new 처리자 기능을 쓰려면 다음과 같이 하면 된다.
// Widget 객체에 대한 메모리 할당이 실패했을 때 호출될 함수의 선언
void outOfMem();
// Widget의 new 처리자 함수로서 outOfMem 설치
Widget::set_new_handler(outOfMem);
// 메모리 할당이 실패하면 outOfMem 호출
Widget* pw1 = new Widget;
// 메모리 할당이 실패하면 전역 new 처리자 함수가 있으면 호출
std::string* ps = new std::string;
// Widget 클래스만을 위한 new 처리자 함수가 아무것도 없도록 설정
Widget::set_new_handler(0);
// 메모리 할당이 실패하면 이제는 예외를 바로 던짐
Widget* pw2 = new Widget;
- 자원 관리 객체를 통한 할당에러 처리를 구현하는 이런 방식의 코드는 어떤 클래스를 쓰더라도 똑같이 나올 것이다.
- 이 코드를 다른 클래스에서도 재사용할 수 있도록 잘 만져 놓으면 좋다.
- 이런 용도에 손쉽게 쓸 수 있는 방법으로 믹스인 양식이 있다.
- 다른 파생 클래스들이 한 가지의 특정 기능만을 물려받아 갈 수 있도록 설계된 기본 클래스이다.
- 지금 경우의 특정 기능은 클래스별 new 처리자를 설정하는 기능이다.
- 그 다음에 그렇게 만든 클래스를 템플릿으로 바꾼다.
- 이렇게 하면 파생 클래스마다 클래스 데이터의 사본이 따로따로 존재하게 된다.
- 이렇게 설계된 클래스 템플릿으로 얻을 수 있는 효과는 다음과 같다.
- 기본 클래스 부분은 파생 클래스들이 가져야 하는
set_new_handler
와operator new
함수를 물려준다. - 템플릿 부분은 각 파생 클래스가(인스턴스화 되면서)
currentHandler
데이터 멤버를 따로따로 가질 수 있게 한다.
- 기본 클래스 부분은 파생 클래스들이 가져야 하는
// 클래스별 set_new_handler를 지원하는 믹스인 양식의 기본 클래스
template<typename T>
class NewHandlerSupport {
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);
// operator new의 다른 버전들을 이 자리에 둔다. 항목 52 참고
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHanlder = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
// 클래스별로 만들어지는 currentHandler 멤버를 널로 초기화
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
class Widget : public NewHandlerSupport<Widget>
{
// set_new_handler, operator new에 대한 선언문은 빠짐
};
NewHandlerSupport<Widget>
으로부터Widget
이 상속을 받는 부분이 있다.NewHandlerSupport
템플릿은 타입 매개변수 T를 아예 쓰지 않는다. 이 템플릿은 T를 쓸 필요가 전혀 없다. 실제로 필요한 것은NewHandlerSupport
로부터 파생된 각 클래스에 대한 객체의 서로 다른 사본(정적 데이터 멤버인currentHandler
의 사본)밖에 없다.이 템플릿의 매개변수인 T는 그냥 파생 클래스들을 구분해 주는 역할만 한다.
어떤 클래스에 클래스 별 new 처리자를 붙이고 싶을 때
NewHandlerSupport
와 같은 템플릿을 쓰면 확실히 쉬운 것은 사실이지만, 믹스인 양식의 상속을 쓰다 보면 어쩔 수 없이 다중 상속 이야기가 끌려 나온다.1993년까지의 C++는
operator new
가 실패하면 널 포인터를 반환하도록 되어 있었다.그러다가 몇 년이 지난 후에
bad_alloc
을 던지도록 명세가 바뀌었는데, 컴파일러 제작사들이 수정된 명세를 지원하려고 폼을 잡을 당시에 이미 많은 C++ 개발도구들이 만들어져 군웅할거하고 있다는 점이 걸림돌이었다.C++ 표준화 위원회는 널 포인터 점검 기반의 콛를 버리고 싶지 않았기 때문에 결국
할당 - 실패시 널 반환
으로 동작하는 대안적인 형태의operator new
도 같이 내놓았다.이런 형태를 가리켜 예외불가(nothrow) 형태라고 하는데, new가 쓰이는 위치에서 이런 함수가 예외를 던지지 않는 객체를 사용한다는 점도 그렇게 불리는 부분적인 이유라고 한다.
class Widget {};
// 할당이 실패하면 bac_alloc 예외를 던짐
Widget *pw1 = new Widget;
if (pw1 == 0) {} // 이 점검 코드는 꼭 실패함
// Widget을 할당하다 실패하면 널을 반환
Widget *pw2 = new (std::nothrow) Widget;
if (pw2 == 0) {} // 이 점검 코드는 성공할 수 있음
항목 50: new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자
- 컴파일러가 열심히 만들어 준
operator new
와operator delete
를 바꾸고 싶은 것일까? - 가장 흔한 이유 세 가지는 다음과 같다.
잘못된 힙 사용 탐지를 위해
- new한 메모리에 delete를 하는 것을 잊어버리면 메모리가 누출되는 것이 이 바닥의 이치이다.
- 한 번 new한 메모리를 두 번 이상 delete하면 미정의 동작이 발생한다.
- 만일 할당된 메모리의 주소 목록을
operator new
가 유지해 두고operator delete
가 그 목록으로부터 주소를 하나씩 제거해 주게 만들어져 있다면, 이런 식의 실수는 쉽게 잡아낼 수 있다. - 데이터 오버런, 데이터 언더런이 발생하는 것에 대비하여 사용자 정의
operator new
를 사용한다면, 요구된 크기보다 약간 더 메모리를 할당한 후에 사용자가 실제로 사용할 메모리의 앞과 뒤에 오버런/언더런 탐지용 바이트 패턴을 적어두도록 만들 수 있다.operator delete
는 누군가 이 경계지표에 손을 댔는지 안 댔는지 점검하도록 만든다.- 만일 이 경계지표에 원래와 다른 정보가 적혀 있다면 할당된 메모리 블록을 사용하는 도중에 오버런이나 언더런이 발생한 것이므로,
operator delete
는 이 사실을 로그로 기록함으로써 문제를 일으킨 포인터 값을 남겨 놓을 수 있다.
효율을 향상시키기 위해
- 컴파일러가 제공하는 기본 버전의
operator new
및operator delete
함수는 대체적으로 일반적인 쓰임새에 맞추어 설계된 것이다. - 실행 기간이 짧지 않은 프로그램에서 잘 돌아가야 하며, 1초 안에 끝나는 프로그램에서도 별 문제가 없어야 한다.
- 메모리 관리자에 대한 요구사항은 가지각색이기 때문에 컴파일러가 기본적으로 제공하는
operator new
함수와operator delete
함수가 지극히 대중적이고 온건지향 스타일의 전략을 취한다. - 개발자가 자신의 프로그램이 동적 메모리를 어떤 성향으로 사용하는지를 제대로 이해하고 있다면, 사용자 정의
operator new
및operator delete
를 사용하는 편이 기본제공 버전을 썼을 때보다 우수한 성능을 낼 확률이 높다.
동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 입맛에 맞게 동작하는 new 및 delete를 무작정 작성해 보겠다고 맨땅에 헤딩부터 하는 것보다는, 여러분이 만든 소프트웨어가 동적 메모리를 어떻게 사용하는지에 관한 정보를 수집하는 세심한 자세가 더 필요하다.
- 할당된 메모리 블록의 크기 분포, 각각의 사용 기간, 메모리가 할당되고 해제되는 순서가 FIFO인지 LIFO인지, 시간 경과에 따라 사용 패턴이 변하는지, 각 실행 단계마다 소프트웨어가 보이는 메모리 할당/해제 패턴이 확연한 차이를 보이는지, 한 번에 실제로 쓰이는 동적 할당 메모리의 최대량 등
- 사용자 정의
operator new
및operator delete
를 사용하면 이런 정보를 아주 쉽게 수집할 수 있다.
메모
항목 49: new 처리자의 동작 원리를 제대로 이해하자
set_new_handler
함수를 쓰면 메모리 할당 요청이 만족되지 못했을 때 호출되는 함수를 지정할 수 있다.- 예외불가(nothrow) new는 영향력이 제한되어 있다. 메모리 할당 자체에만 적용되기 때문이다. 이후에 호출되는 생성자에서는 얼마든지 예외를 던질 수 있다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(370p ~ 381p) (1) | 2024.02.14 |
---|---|
이펙티브 C++(360p ~ 369p) (0) | 2024.02.13 |
이펙티브 C++(340p ~ 350p) (1) | 2024.02.10 |
이펙티브 C++(332p ~ 340p) (1) | 2024.02.09 |
이펙티브 C++(312p ~ 320p) (1) | 2024.02.08 |