ISO C ++ 표준을 준수하는 사용자 정의 new 및 delete 연산자를 어떻게 작성해야합니까?
ISO C ++ 표준을 준수하는 사용자 지정 new
및 delete
연산자를 어떻게 작성해야 합니까?
이것은 C ++ FAQ, Operator overloading 및 후속 작업 에서 Overloading new 및 delete의 연속입니다 . 왜 기본 new 및 delete 연산자를 대체해야합니까?
섹션 1 : 표준 준수 new
연산자 작성
섹션 2 : 표준 준수 delete
연산자 작성
(참고 : 이것은 Stack Overflow의 C ++ FAQ에 대한 항목 입니다.이 양식으로 FAQ를 제공하는 아이디어를 비판하고 싶다면이 모든 것을 시작한 메타에 게시 할 수 있습니다. 이 질문은 FAQ 아이디어가 처음 시작된 C ++ 채팅룸 에서 모니터링 되므로 아이디어를 제안한 사람들이 답변을 읽을 가능성이 매우 높습니다.)
참고 : 답변은 Scott Meyers의 학습을 기반으로합니다. '보다 효과적인 C ++ 및 ISO C ++ 표준.
파트 I
이 C ++ FAQ 항목 은 자신의 클래스에 대해 오버로드 및 연산자를 원하는 이유를 설명했습니다 . 이 현재 FAQ는 표준을 준수하는 방법 을 설명 합니다 .new
delete
사용자 지정 new
연산자 구현
C ++ 표준 (§18.4.1.1)은 다음 operator new
과 같이 정의 합니다.
void* operator new (std::size_t size) throw (std::bad_alloc);
C ++ 표준은 이러한 연산자의 사용자 지정 버전이 §3.7.3 및 §18.4.1에서 준수해야하는 의미 체계를 지정합니다.
요구 사항을 요약 해 보겠습니다.
요구 사항 # 1 : 최소 size
바이트의 메모리를 동적으로 할당 하고 할당 된 메모리에 대한 포인터를 반환해야합니다. C ++ 표준, 섹션 3.7.4.1.3에서 인용 :
할당 기능은 요청 된 스토리지 양을 할당하려고합니다. 성공하면 바이트 단위의 길이가 요청 된 크기 이상인 스토리지 블록의 시작 주소를 반환합니다.
이 표준은 다음을 추가로 부과합니다.
... 반환 된 포인터는 모든 완전한 객체 유형의 포인터로 변환 될 수 있도록 적절하게 정렬되어야하며 할당 된 스토리지의 객체 또는 배열에 액세스하는 데 사용됩니다 (스토리지가 해당하는 호출에 의해 명시 적으로 할당 해제 될 때까지) 할당 해제 기능). 요청 된 공간의 크기가 0이더라도 요청이 실패 할 수 있습니다. 요청이 성공하면, 반환 된 값은 이전에 반환 된 값 p1과 다른 null이 아닌 포인터 값 (4.10) p0이어야합니다
delete
.
이것은 우리에게 더 중요한 요구 사항을 제공합니다
요구 사항 # 2 : 우리가 사용하는 메모리 할당 함수 (일반적으로 malloc()
또는 다른 사용자 지정 할당 자)는 할당 된 메모리에 대해 적절하게 정렬 된 포인터를 반환해야 합니다.이 포인터는 완전한 개체 유형의 포인터로 변환되어 개체에 액세스하는 데 사용될 수 있습니다.
요구 사항 # 3 : 사용자 지정 연산자 new
는 0 바이트가 요청 된 경우에도 올바른 포인터를 반환해야합니다.
new
프로토 타입 에서 추론 할 수있는 분명한 요구 사항 중 하나 는 다음과 같습니다.
요구 사항 # 4 :new
요청 된 크기의 동적 메모리를 할당 할 수없는 경우 유형의 예외를 throw해야합니다 std::bad_alloc
.
그러나! 눈에 보이는 것보다 더 많은 것이 있습니다. new
운영자 문서를 자세히 살펴보면 (표준에서 인용 한 내용이 더 아래로 이어짐) 다음과 같이 명시됩니다.
경우 set_new_handler가 정의하는 데 사용 된 new_handler의 기능을,이
new_handler
기능의 표준 기본 정의에 의해 불려operator new
가 그 자신에 의해 요청 된 스토리지를 할당 할 수없는 경우.
사용자 지정 new
이이 요구 사항을 지원 하는 데 필요한 방법을 이해하려면 다음을 이해해야합니다 .
무엇인가 new_handler
와 set_new_handler
?
new_handler
소요 아무 것도 반환하지 않으며, 함수에 대한 포인터에 대한 형식 정의입니다 set_new_handler
취해을 반환하는 함수입니다 new_handler
.
set_new_handler
의 매개 변수는 요청 된 메모리를 할당 할 수없는 경우 new가 호출해야하는 함수 연산자에 대한 포인터입니다. 반환 값은 이전에 등록 된 핸들러 함수에 대한 포인터이거나 이전 핸들러가없는 경우 null입니다.
코드 샘플이 일을 명확하게 할 수있는 적절한 순간 :
#include <iostream>
#include <cstdlib>
// function to call if operator new can't allocate enough memory or error arises
void outOfMemHandler()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
//set the new_handler
std::set_new_handler(outOfMemHandler);
//Request huge memory size, that will cause ::operator new to fail
int *pBigDataArray = new int[100000000L];
return 0;
}
위의 예에서 operator new
(대부분) 100,000,000 정수에 대한 공간을 할당 할 수 없으며 함수 outOfMemHandler()
가 호출되고 프로그램은 오류 메시지를 발행 한 후 중단됩니다 .
이 때 점에 유의하는 것이 중요 operator new
메모리 요청을 이행 할 수없는, 그것은 호출 new-handler
이 될 때까지 반복 기능을 수있는 충분한 메모리를 찾거나 더 이상 새로운 핸들러가 없습니다. 위의 예에서 우리가 필요하지 않은 경우 std::abort()
, outOfMemHandler()
될 것이다 반복적으로 호출 . 따라서 핸들러는 다음 할당이 성공했는지 확인하거나 다른 핸들러를 등록하거나 핸들러를 등록하지 않거나 리턴하지 않아야합니다 (예 : 프로그램 종료). 새 핸들러가없고 할당에 실패하면 연산자가 예외를 발생시킵니다.
파트 II
operator new
예제 의 동작을 감안할 때 잘 설계된 사람은 다음 중 하나를 수행 new_handler
해야합니다 .
더 많은 메모리를 사용할 수 있도록합니다 . 이렇게하면 new operator의 루프 내에서 다음 메모리 할당 시도가 성공할 수 있습니다. 이를 구현하는 한 가지 방법은 프로그램 시작시 큰 메모리 블록을 할당 한 다음 new-handler가 처음 호출 될 때 프로그램에서 사용하도록 해제하는 것입니다.
다른 새 처리기 설치 : 현재 새 처리기가 더 이상 메모리를 사용할 수없고 사용할 수있는 다른 새 처리기가있는 경우 현재 새 처리기가 그 자리에 다른 새 처리기를 설치할 수 있습니다 ( )를 호출하여 set_new_handler
. 다음 번에 new operator가 new-handler 기능을 호출하면 가장 최근에 설치된 기능을 가져옵니다.
(이 테마의 변형은 새 처리자가 자신의 동작을 수정하는 것이므로 다음에 호출 할 때 다른 작업을 수행합니다.이를 달성하는 한 가지 방법은 새 처리자가 정적, 네임 스페이스 별 또는 신규 처리자의 행동에 영향을 미치는 글로벌 데이터.)
새 처리기 제거 : 이것은 null 포인터를에 전달하여 수행됩니다 set_new_handler
. 새 핸들러가 설치되지 않은 경우 메모리 할당에 실패 operator new
하면 예외 ((convertible to) std::bad_alloc
)가 발생합니다.
로 변환 할 수 있는 예외를 발생std::bad_alloc
시킵니다. 이러한 예외는에서 포착되지 operator new
않지만 메모리 요청을 시작하는 사이트로 전파됩니다.
반환하지 : 호출하여 abort
나 exit
.
특정 클래스를 구현하려면 new_handler
자체 버전 set_new_handler
및 operator new
. 클래스 set_new_handler
는 클라이언트가 클래스에 대한 new-handler set_new_handler
를 지정할 수 있도록 합니다 (표준이 클라이언트가 전역 new-handler를 지정할 수 있도록 허용 하는 것과 정확히 같습니다 ). 클래스 operator new
는 클래스 객체에 대한 메모리가 할당 될 때 전역 뉴 핸들러 대신 클래스 특정 뉴 핸들러가 사용되도록 보장합니다.
이제 우리는 이해 new_handler
및 set_new_handler
더 나은 우리가 수정할 수 있습니다 요구 사항 # 4 로 적합 :
요구 사항 # 4 (고급)은 :
우리는operator new
각 실패 후 새로운 처리 함수를 호출, 두 번 이상 메모리를 할당하려고합니다. 여기서 가정은 새로운 처리 기능이 메모리를 확보하기 위해 무언가를 할 수 있다는 것입니다. 새로운-처리 함수에 대한 포인터 인 경우에만null
않는operator new
예외를 throw합니다.
약속 한대로 표준의 인용 :
섹션 3.7.4.1.3 :
스토리지 할당에 실패한 할당 함수는 현재 설치된
new_handler
(18.4.2.2
)을 호출 할 수 있습니다 . [참고 : 프로그램에서 제공하는 할당 함수는 함수 ( )를new_handler
사용하여 현재 설치된 주소를 얻을 수 있습니다 .] 비어있는 예외 사양 ( ),으로 선언 된 할당 함수가 스토리지 할당에 실패하면 널 포인터를 반환합니다. . 스토리지 할당에 실패한 다른 할당 함수는 클래스 ( ) 또는에서 파생 된 클래스의 예외를 throw하여 실패를 나타냅니다 .set_new_handler
18.4.2.3
15.4
throw()
std::bad_alloc
18.4.2.1
std::bad_alloc
# 4 요구 사항으로 무장 한 다음 우리의 의사 코드를 시도해 보겠습니다 new operator
.
void * operator new(std::size_t size) throw(std::bad_alloc)
{
// custom operator new might take additional params(3.7.3.1.1)
using namespace std;
if (size == 0) // handle 0-byte requests
{
size = 1; // by treating them as
} // 1-byte requests
while (true)
{
//attempt to allocate size bytes;
//if (the allocation was successful)
//return (a pointer to the memory);
//allocation was unsuccessful; find out what the current new-handling function is (see below)
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) //If new_hander is registered call it
(*globalHandler)();
else
throw std::bad_alloc(); //No handler is registered throw an exception
}
}
파트 III
새로운 핸들러 함수 포인터를 직접 얻을 수는 없습니다 set_new_handler
. 그것이 무엇인지 알아 내기 위해 호출 해야합니다. 이것은 조잡하지만 적어도 단일 스레드 코드의 경우 효과적입니다. 다중 스레드 환경에서는 새 처리 함수 뒤에있는 (전역) 데이터 구조를 안전하게 조작하기위한 일종의 잠금이 필요할 것입니다. ( 더 많은 인용 / 세부 사항을 환영합니다. )
또한 무한 루프가 있고 루프를 빠져 나가는 유일한 방법은 메모리를 성공적으로 할당하거나 새 처리 함수가 이전에 추론 한 작업 중 하나를 수행하는 것입니다. new_handler
이 중 하나가 수행 되지 않는 한, new
연산자 내부의이 루프 는 종료되지 않습니다.
주의 사항 : 표준 ( §3.7.4.1.3
, 위에 인용 됨)은 오버로드 된 new
연산자 가 무한 루프를 구현 해야 한다고 명시 적으로 말하지 않지만 기본 동작이라고 만 말합니다. 따라서이 세부 사항은 해석이 가능하지만 대부분의 컴파일러 ( GCC 및 Microsoft Visual C ++ )는이 루프 기능을 구현합니다 (앞서 제공된 코드 샘플을 컴파일 할 수 있음). 또한 Scott Meyers 와 같은 C ++ 권위자 가이 접근 방식을 제안하므로 충분히 합리적입니다.
특별 시나리오
다음 시나리오를 고려해 보겠습니다.
class Base
{
public:
static void * operator new(std::size_t size) throw(std::bad_alloc);
};
class Derived: public Base
{
//Derived doesn't declare operator new
};
int main()
{
// This calls Base::operator new!
Derived *p = new Derived;
return 0;
}
As this FAQ, explains, a common reason for writing a custom memory manager is to optimize allocation for objects of a specific class, not for a class or any of its derived classes, which basically means that our operator new for the Base class is typically tuned for objects of size sizeof(Base)
-nothing larger and nothing smaller.
In the above sample, because of inheritance the derived class Derived
inherits the new operator of the Base class. This makes calling operator new in a base class to allocate memory for an object of a derived class possible. The best way for our operator new
to handle this situation is to divert such calls requesting the "wrong" amount of memory to the standard operator new, like this:
void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base)) // If size is "wrong,", that is, != sizeof Base class
{
return ::operator new(size); // Let std::new handle this request
}
else
{
//Our implementation
}
}
Note that, the check for size also incoprporates our requirement #3. This is because all freestanding objects have a non-zero size in C++, so sizeof(Base)
can never be zero, so if size is zero, the request will be forwarded to ::operator new
, and it is gauranteed that it will handle it in standard compliant way.
Citation: From the creator of C++ himself, Dr Bjarne Stroustrup.
Implementing a custom delete operator
The C++ Standard(§18.4.1.1
) library defines operator delete
as:
void operator delete(void*) throw();
Let us repeat the exercise of gathering the requirements for writing our custom operator delete
:
Requirement #1: It shall return void
and its first parameter shall be void*
. A custom delete operator
can have more than one parameter as well but well we just need one parameter to pass the pointer pointing to the allocated memory.
Citation from the C++ Standard:
Section §3.7.3.2.2:
"Each deallocation function shall return void and its first parameter shall be void*. A deallocation function can have more than one parameter....."
Requirement #2: It should guarantee that it is safe to delete a null pointer passed as an argument.
Citation from C++ Standard: Section §3.7.3.2.3:
The value of the first argument supplied to one of the deallocation functions provided in the standard library may be a null pointer value; if so, the call to the deallocation function has no effect. Otherwise, the value supplied to
operator delete(void*)
in the standard library shall be one of the values returned by a previous invocation of eitheroperator new(size_t)
oroperator new(size_t, const std::nothrow_t&)
in the standard library, and the value supplied tooperator delete[](void*)
in the standard library shall be one of the values returned by a previous invocation of eitheroperator new[](size_t)
oroperator new[](size_t, const std::nothrow_t&)
in the standard library.
Requirement #3: If the pointer being passed is not null
, then the delete operator
should deallocate the dynamic memory allocated and assigned to the pointer.
Citation from C++ Standard: Section §3.7.3.2.4:
If the argument given to a deallocation function in the standard library is a pointer that is not the null pointer value (4.10), the deallocation function shall deallocate the storage referenced by the pointer, render-ing invalid all pointers referring to any part of the deallocated storage.
Requirement #4: Also, since our class-specific operator new forwards requests of the "wrong" size to ::operator new
, We MUST forward "wrongly sized" deletion requests to ::operator delete
.
So based on the requirements we summarized above here is an standard conformant pseudo code for a custom delete operator
:
class Base
{
public:
//Same as before
static void * operator new(std::size_t size) throw(std::bad_alloc);
//delete declaration
static void operator delete(void *rawMemory, std::size_t size) throw();
void Base::operator delete(void *rawMemory, std::size_t size) throw()
{
if (rawMemory == 0)
{
return; // No-Op is null pointer
}
if (size != sizeof(Base))
{
// if size is "wrong,"
::operator delete(rawMemory); //Delegate to std::delete
return;
}
//If we reach here means we have correct sized pointer for deallocation
//deallocate the memory pointed to by rawMemory;
return;
}
};
'IT TIP' 카테고리의 다른 글
C #의 간단한 스레드 풀에 대한 코드 (0) | 2020.12.01 |
---|---|
Mercurial에서 저장소의 하위 폴더를 어떻게 복제합니까? (0) | 2020.12.01 |
Ansible-디렉토리의 경우 모드 755, 파일의 경우 재귀 적으로 644 (0) | 2020.11.30 |
Python, Pandas : DataFrame의 내용을 텍스트 파일에 기록 (0) | 2020.11.30 |
C # /. NET에서 경로와 파일 이름을 결합하는 가장 좋은 방법은 무엇입니까? (0) | 2020.11.30 |