new
およびdelete
演算子をどのように記述すればよいですか?これは、非常に明快なC++のFAQの Overloading new and delete の続きです Operator overloading とそのフォローアップ なぜデフォルトのnewと演算子を削除しますか?
セクション1:標準に準拠したnew
演算子の記述
セクション2:標準に準拠したdelete
演算子の記述
(注:これは Stack OverflowのC++ FAQ)へのエントリであることを意味します 。このフォームにFAQを提供するという考えを批評したい場合、次に これをすべて開始したメタへの投稿 がその場所になります。その質問への回答は C++チャットルーム で監視され、ここでFAQアイデアは最初から始まっていたので、アイデアを思いついた人があなたの答えを読む可能性は非常に高いです。)
注:回答は、スコットマイヤーズのより効果的なC++およびISO C++標準からの学習に基づいています。
例の_operator new
_の動作を考えると、適切に設計された_new_handler
_ mustは次のいずれかを実行します。
使用可能なメモリを増やす:これにより、オペレーターの新しいループ内の次のメモリ割り当てが成功する可能性があります。これを実装する1つの方法は、プログラムの起動時に大きなメモリブロックを割り当て、それを解放して、新しいハンドラーが最初に呼び出されたときにプログラムで使用できるようにすることです。
別の新しいハンドラーをインストールします:現在の新しいハンドラーがこれ以上メモリを使用できない場合、および使用可能な別の新しいハンドラーがある場合、現在の新しいハンドラーが他の新しいハンドラーをインストールできますその代わりにnew-handler(_set_new_handler
_を呼び出す)。オペレーターnewが次にnew-handler関数を呼び出すときに、最後にインストールされたものを取得します。
(このテーマのバリエーションは、新しいハンドラーが独自の動作を変更するためのものです。そのため、次に呼び出されたときに、別の動作をします。これを実現する1つの方法は、新しいハンドラーが静的、名前空間固有、またはnew-handlerの動作に影響を与えるグローバルデータ)
new-handlerのアンインストール:これは、ヌルポインタを_set_new_handler
_に渡すことによって行われます。新しいハンドラがインストールされていない場合、メモリの割り当てに失敗すると、_operator new
_は例外をスローします((変換可能)_std::bad_alloc
_)。
例外をスロー _std::bad_alloc
_に変換可能。そのような例外は_operator new
_によってキャッチされませんが、メモリの要求を発信するサイトに伝播します。
返されません:abort
またはexit
を呼び出します。
クラス固有の_new_handler
_を実装するには、クラスに_set_new_handler
_および_operator new
_の独自のバージョンを提供する必要があります。クラスの_set_new_handler
_を使用すると、クライアントはクラスの新しいハンドラを指定できます(標準の_set_new_handler
_とまったく同じように、クライアントはグローバルな新しいハンドラを指定できます)。クラスの_operator new
_は、クラスオブジェクトのメモリが割り当てられるときに、グローバルの新しいハンドラーの代わりにクラス固有の新しいハンドラーが使用されるようにします。
_new_handler
_と_set_new_handler
_をよく理解したところで、次のように要件#4を適切に変更できます。
要件#4(拡張):
私たちの_operator new
_は、メモリを2回以上割り当てようとし、失敗するたびに新しい処理関数を呼び出します。ここでの前提は、新しい処理関数が何らかのメモリを解放するために何かを実行できる可能性があるということです。新しい処理関数へのポインタがnull
である場合のみ、_operator new
_は例外をスローします。
約束どおり、規格からの引用:
セクション3.7.4.1.3:
ストレージの割り当てに失敗した割り当て関数は、現在インストールされている_
new_handler
_(_18.4.2.2
_)があれば、それを呼び出すことができます。 [注:プログラム提供の割り当て関数は、_new_handler
_関数(_set_new_handler
_)を使用して、現在インストールされている_18.4.2.3
_のアドレスを取得できます。]空の例外指定で宣言された割り当て関数の場合(_15.4
_)、throw()
、ストレージの割り当てに失敗し、nullポインターを返します。ストレージの割り当てに失敗した他の割り当て関数は、クラス_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
}
}
_
新しいハンドラー関数ポインターを直接取得できないことに注意してください。_set_new_handler
_を呼び出して、それが何であるかを調べる必要があります。これは粗雑ですが効果的ですが、少なくともシングルスレッドのコードではそうです。マルチスレッド環境では、おそらく、新しい処理関数の背後にある(グローバル)データ構造を安全に操作するための何らかのロックが必要になります。 (これについては、より多くの引用/詳細を歓迎します。)
また、無限ループがあり、ループから抜け出す唯一の方法は、メモリが正常に割り当てられるか、新しい処理関数が以前に推測したことの1つを実行することです。 _new_handler
_がこれらのことを行わない限り、new
演算子内のこのループは終了しません。
注意:標準(_§3.7.4.1.3
_、上で引用)は、オーバーロードされたnew
演算子mustが無限ループを実装することを明示的に述べていないことに注意してください、しかしそれは単にそれがデフォルトの振る舞いであると言っているだけです。 したがって、この詳細は解釈の自由ですが、ほとんどのコンパイラ( [〜#〜] 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;
}
_
this FAQで説明されているように、カスタムメモリマネージャーを作成する一般的な理由は、クラスやその派生クラスではなく、特定のクラスのオブジェクトの割り当てを最適化することですつまり、基本的には、Baseクラスの新しい演算子は通常、サイズがsizeof(Base)
のオブジェクト用に調整されています。
上記のサンプルでは、継承のため、派生クラスDerived
はBaseクラスの新しい演算子を継承します。これにより、派生クラスのオブジェクトにメモリを割り当てるために、基本クラスで演算子を新しく呼び出すことが可能になります。 _operator new
_がこの状況を処理する最善の方法は、次のように、「間違った」量のメモリを要求する呼び出しを標準オペレーターnewに転送することです。
_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
}
}
_
サイズのチェックには要件#も含まれていないことに注意してください。これは、C++ではすべての独立オブジェクトがゼロ以外のサイズであるため、sizeof(Base)
がゼロになることはないため、サイズがゼロの場合、リクエストは_::operator new
_に転送され、保証されます。標準に準拠した方法で処理すること。
C++標準(_§18.4.1.1
_)ライブラリは_operator delete
_を次のように定義します。
_void operator delete(void*) throw();
_
カスタム_operator delete
_を記述するための要件を収集する練習を繰り返しましょう。
要件#1:void
を返し、最初のパラメーターは_void*
_でなければなりません。カスタム_delete operator
_にも複数のパラメーターを含めることができますが、割り当てられたメモリを指すポインターを渡すために必要なパラメーターは1つだけです。
C++標準からの引用:
セクション3.7.3.2.2:
「各割り当て解除関数はvoidを返し、その最初のパラメーターはvoid *でなければなりません。割り当て解除関数は複数のパラメーターを持つことができます....」
要件#2:引数として渡されたnullポインターを削除しても安全であることを保証する必要があります。
C++標準からの引用:セクション3.7.3.2.3:
標準ライブラリで提供される割り当て解除関数の1つに提供される最初の引数の値は、nullポインタ値である可能性があります。その場合、割り当て解除関数の呼び出しは効果がありません。それ以外の場合、標準ライブラリの
operator delete(void*)
に提供される値は、標準ライブラリのoperator new(size_t)
またはoperator new(size_t, const std::nothrow_t&)
の以前の呼び出しによって返された値の1つでなければなりません。そして、標準ライブラリのoperator delete[](void*)
に提供される値は、標準ライブラリのoperator new[](size_t)
またはoperator new[](size_t, const std::nothrow_t&)
の以前の呼び出しによって返された値の1つでなければなりません。
要件#3:渡されるポインターがnull
でない場合、_delete operator
_は、ポインターに割り当てられ割り当てられている動的メモリの割り当てを解除する必要があります。
C++標準からの引用:セクション3.7.3.2.4:
標準ライブラリの割り当て解除関数に与えられた引数がnullポインタ値(4.10)ではないポインタである場合、割り当て解除関数は、ポインタが参照するストレージの割り当てを解除し、の一部を参照するすべてのポインタを無効にします割り当て解除されたストレージ。
要件#4:また、クラス固有の演算子newは「間違った」サイズのリクエストを_::operator new
_に転送するため、We [〜#〜] must [〜#〜 ]「誤ったサイズの」削除リクエストを_::operator delete
_に転送します。
したがって、上記で要約した要件に基づいて、カスタム_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;
}
};
_