일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 반복자
- exception
- Effective c++
- reference
- more effective c++
- operator new
- 언리얼
- lua
- 예외
- Smart Pointer
- 비교 함수 객체
- 상속
- 암시적 변환
- 참조자
- 메타테이블
- 오블완
- virtual function
- 루아
- 영화 리뷰
- Vector
- 게임
- resource management class
- c++
- 영화
- implicit conversion
- 스마트 포인터
- effective stl
- 다형성
- 티스토리챌린지
- UE4
Archives
- Today
- Total
스토리텔링 개발자
[Effective C++] 51. operator new / delete 커스텀 관례 본문
728x90
항목 51. new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아두자
operator new 구현 시 요구사항
- 반환값이 제대로 되어 있어야 한다.
- 가용 메모리가 부족할 경우 new 처리자 함수를 호출해야 한다. (항목 49 참조)
- 크기가 없는(0바이트) 메모리 요청에 대한 대비를 해야 한다.
- 실수로 ‘기본(normal)’ 형태의 new가 가려지지 않도록 해야 한다. (항목 52 참조)
요구사항을 지키며 operator new 구현
- 구현?
- 요청된 메모리를 마련해 줄 수 있는 경우
- 그 메모리에 대한 포인터를 반환한다.
- 요청된 메모리를 마련해 줄 수 없는 경우
- bad_alloc 타입의 예외를 던지면 된다. (항목 49 참조)
- 말로는 쉽지만 실제 구현은 꽤 까다롭다..
- 요청된 메모리를 마련해 줄 수 있는 경우
- 문제점
- operator new는 메모리 할당이 실패할 때마다 new 처리자 함수를 호출한다.
- 그러면서 메모리 할당을 2회 이상 시도하게 된다.(operator new 내부에는 무한 루프가 있기 때문이다.)
- 즉, 할당 실패한 메모리에 대한 해제를 new 처리자 함수 쪽에서 맡을 것으로 가정한다.
- operator new가 예외를 던지는 상황은 오직 new 처리자 함수에 대한 포인터가 null일 때 뿐이다.
- 0바이트가 요구되었을 때조차도 operator new 함수는 적법한 포인터를 반환해야 한다.
- operator new는 메모리 할당이 실패할 때마다 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();
}
}
- 사용된 트릭
- 외부에서 0바이트를 요구했을 때 1바이트 요구인 것으로 간주하고 처리한다.
- new 처리자 함수를 가져오기 위해 널로 설정하고 바로 뒤에서 되돌려 놓는다.
- 안타깝지만 현재의 전역 new 처리자 함수를 얻어오는 직접적인 방법이 없다?
- 모던 C++에서는 std::get_new_handler를 지원한다.
- operator new 함수에는 무한 루프가 들어있다.
- 무한루프를 빠져나오는 조건
- 메모리 할당이 성공한다
- 아래 동작 중 하나를 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[] 안에서 해줄 일은 단순히 원시 메모리의 덩어리를 할당하는 것 밖엔 없다.
- 원시 메모리 덩어리 할당만 가능한 이유
- 이 시점에서는 배열 메모리에 아직 생기지도 않은 클래스 객체에 대해 아무 것도 할 수 없다.
- 배열 안에 몇 개의 객체가 들어갈지 계산조차 불가능하다.
- 객체 하나가 얼마나 클지 확정할 방법이 없기 때문이다.
- 파생 클래스 객체에서도 operator new[] 가 호출될 수 있다는 점 때문이다.
- 그러므로 Base::operator new[] 안에서조차 배열 객체 하나의 크기가 sizeof(Base)라는 가정을 할 수 없다.
- 즉, 객체의 갯수를 (요구된 바이트 수 / sizeof(Base)) 로 계산할 수 없다는 뜻이다.
- 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
'개발 > Effective C++' 카테고리의 다른 글
[Effective C++] 53. 컴파일러 경고 문제 (0) | 2024.07.29 |
---|---|
[Effective C++] 52. 위치지정 new / delete (0) | 2024.07.26 |
[Effective C++] 50. operator new / delete는 언제 커스텀해야 할까? (2) | 2024.07.24 |
[Effective C++] 49. new 처리자 (6) | 2024.07.23 |
[Effective C++] 48. 템플릿 메타 프로그래밍 (0) | 2024.07.22 |
Comments