C++プログラミング言語における配置構文とは、個々のオブジェクトのメモリ管理(つまり、メモリ内での「配置」)を明示的に指定するための構文のことです。通常、オブジェクトが動的に作成される場合、割り当て関数が呼び出され、オブジェクト用のメモリが割り当てられ、そのメモリ内でオブジェクトが初期化されます。配置構文を使用すると、プログラマは割り当て関数に追加の引数を指定できます。よく使われる例としては、オブジェクトを初期化できる適切な記憶領域へのポインタを指定することで、メモリ割り当てとオブジェクト構築を分離することが挙げられます。
new演算子deleteと関数の「配置」バージョンは、配置newと配置として知られていますdelete。[ 1 ]式は、配置であるかどうかにかかわらず、関数を呼び出します。このnew関数は、アロケータ関数とも呼ばれ、その名前はです。同様に、式は関数を呼び出します。この関数は、デアロケータ関数とも呼ばれ、その名前はです。[ 2 ] [ 3 ]newoperator newdeletedeleteoperator delete
new配置構文を使用する式はすべて配置new式であり、必須の最初のパラメータ()よりも多くのパラメータを受け取る関数はoperator newすべて配置または配置関数です。[ 4 ]配置関数は、2 つの入力パラメータ(および)を受け取ります。operator deletesize_tnewdeletenewsize_tvoid*
C++ の初期バージョンでは、配置newというものは存在せず、代わりに開発者はthisコンストラクタ内で明示的な代入を使用して同様の効果を実現していました。[ 5 ]この方法は C++98 で廃止され削除されており、Stroustrup の著書「The C++ Programming Language 」の第 3 版ではこの手法について言及されていません。
new new-type-id ( optional-initializer-expression-list )配置構文は、キーワードの直後に式リストを追加しますnew。この式リストが配置です。式はいくつでも含めることができます。[ 2 ] [ 3 ] [ 6 ]
new ( expression-list ) new-type-id ( optional-initializer-expression-list )配置new関数は、非配置関数のオーバーロードnewです。非配置new関数の宣言は、それぞれ非配列式と配列new式に対して次のようになります。[ 7 ] [ 8 ]
void * operator new ( size_t n ); void * operator new []( size_t n );C ++ 標準ライブラリは、これらの関数それぞれに対して 2 つの配置オーバーロードを提供します。それらの宣言は次のとおりです。[ 7 ] [ 8 ]
void * operator new ( size_t n , const nothrow_t & t ) noexcept ; void * operator new ( size_t n , void * p ) noexcept ; void * operator new []( size_t n , const nothrow_t & t ) noexcept ; void * operator new []( size_t n , void * p ) noexcept ;すべてのオーバーロードにおいて、関数の最初のパラメータはoperator new型でありsize_t、関数が呼び出されたときに、割り当てるメモリ量(バイト単位)を指定する引数として渡されます。すべての関数は型を返す必要がありvoid*、これは関数が割り当てるストレージへのポインタです。 [ 2 ]
配置関数もありますdelete。これらは非配置delete関数のオーバーロード版です。非配置delete関数は次のように宣言されます。[ 7 ] [ 8 ]
void operator delete ( void * p ) noexcept ; void operator delete []( void * p ) noexcept ;標準ライブラリは、これらの関数それぞれに対して2つの配置オーバーロードを提供します。それらの宣言は次のとおりです。[ 7 ] [ 8 ]
void operator delete ( void * p , const nothrow_t & t ) noexcept ; void operator delete ( void * p , void * q ) noexcept ; void operator delete []( void * p , const nothrow_t & t ) noexcept ; void operator delete []( void * p , void * q ) noexcept ;すべてのオーバーロードにおいて、関数の最初のパラメータはoperator delete型でありvoid*、これは解放するストレージのアドレスです。[ 2 ]
配置構文には、デフォルトの配置、例外の防止、カスタムアロケータ、デバッグという4つの主な用途があります。
operator newおよび のoperator delete追加パラメータを使用する配置オーバーロードは、デフォルトの配置、つまりポインタ配置void*として使用されます。標準ライブラリによるそれらの定義は、C++ プログラムが置き換えたりオーバーライドしたりすることは許可されていません。[ 7 ] [ 8 ] [ 9 ]
void * operator new ( size_t n , void * p ) noexcept { return p ; }void * operator new []( size_t n , void * p ) noexcept { return p ; }void operator delete ( void * p , void * q ) noexcept {} void operator delete []( void * p , void * q ) noexcept {}デフォルト配置には様々な用途がある。
ビャルネ・ストロヴストルップは著書『C++の設計と進化』の中で、new特定のハードウェアアドレスに特定のオブジェクトが存在することを期待するハードウェアにはポインタ配置が必要であると最初に指摘した。また、マルチプロセッサコンピュータの複数のプロセッサ間で共有される領域など、特定のメモリ領域に存在する必要のあるオブジェクトの構築にも必要である。[ 10 ]
しかし、他の用途としては、C++言語では通常は許可されていないコンストラクタの直接呼び出しなどが挙げられる。[ 3 ]
C++ 言語では、プログラムがデストラクタを直接呼び出すことができます。式を使用してオブジェクトを破棄することはできないためdelete、ポインタ配置new式を介して構築されたオブジェクトを破棄する方法は次のとおりです。[ 11 ] [ 12 ]p->~X();
配置はnew、演算子がメモリを割り当てるのを防ぐnew(例えば、メモリが事前に割り当てられていて、そこにオブジェクトを配置したい場合など)が、オブジェクト自体は構築するために使用されます。このような配置が必要となる典型的な状況の例は次のとおりです。
std::vector<T>(reserve()メソッドを参照) を実装する場合など。コンストラクタは、他の関数とは異なり、オブジェクトが存在しないとき(メモリのみが存在するとき)に呼び出され、実行が完了すると、メモリは完全に初期化されたオブジェクトになります。したがって、コンストラクタはオブジェクトに対して呼び出すことはできませんが、非静的メンバにアクセス(および初期化)する必要があります。このため、コンストラクタを直接呼び出すとエラーになります。解決策は、演算子の配置形式ですnew。
この演算子は次のように実装されています。
void * operator new ( size_t count , void * here ) noexcept { return here ; }void * operator new []( size_t count , void * here ) noexcept { return here ; }通常、(非配置)関数は、使用可能なメモリがすべて枯渇するなどのエラーに遭遇すると、new型の例外をスローします。これは、Stroustrup のAnnotated C++ Reference Manualで関数が定義されていた方法ではなく、C++ 言語が標準化されたときに標準化委員会によって行われた変更です。エラーが発生したときにnull ポインタを返すという関数の元の動作は、配置構文を使用してアクセスできます。[ 3 ] [ 4 ] [ 6 ]std::bad_alloc
これを使用するには、標準ライブラリのヘッダー(または同等のモジュール)が必要です。このヘッダーは、グローバルオブジェクトを宣言します。このオブジェクトは、型(ヘッダーでも宣言されています)であり、2 番目のパラメータとして受け取るオーバーロードされた関数を呼び出すために使用されます。例:[ 9 ]<new>stdstd::nothrowstd::nothrow_tnewconst std::nothrow_t&
import std ;std :: nothrowを使用します。struct X { // ... };int main () { // 関数 operator new(size_t, const nothrow_t&) を呼び出し、(成功した場合)オブジェクトを構築します。X * p = new ( nothrow ) X ; if ( p ) { // ストレージが割り当てられ、コンストラクタが呼び出されました。delete p ; } else { // エラーが発生しました。ストレージが割り当てられておらず、オブジェクトも構築されていません。} return 0 ; }配置構文はカスタムアロケータにも使用されます。これは標準ライブラリヘッダーのアロケータ関数とデアロケータ関数を一切使用せず<new>、プログラマがユーザー定義型用にオーバーロードされた独自の割り当て関数とデアロケータ関数を記述する必要があります。たとえば、次のようにアリーナアロケータクラスを定義できます。 [ 7 ] [ 8 ]
import std ;class Arena { public : // ...void * allocate ( size_t n ) { // メモリ割り当てメソッド... }void deallocate ( void * p ) { // 解放メソッド... } };アリーナの割り当ては、C++ 標準ライブラリのstd::pmr::monotonic_buffer_resourceクラスによって提供されることに注意してください。[ 13 ]
同様に、カスタム配置割り当て関数と割り当て解除関数を次のように定義します。[ 7 ] [ 8 ]
void * operator new ( size_t size , Arena & arena ) { return arena . allocate ( size ); }void operator delete ( void * p , Arena & arena ) { arena . deallocate ( p ); }プログラムは、Arena次のようにクラスの異なるインスタンスを使用してオブジェクトを割り当てるために配置構文を使用します。[ 7 ] [ 8 ]
アリーナarena1 ;アリーナarena2 ; X * p1 = new ( arena1 ) X ; X * p2 = new ( arena2 ) X ;このような方法でストレージが割り当てられたオブジェクトを破棄するには、注意が必要です。配置式がないためdelete、それを使用してカスタムデアロケータを呼び出すことはできません。カスタムデアロケータを呼び出す破棄関数を作成するか、delete関数呼び出しとして配置関数を直接呼び出す必要があります。[ 11 ] [ 7 ] [ 8 ]
前者は次のようになるだろう:[ 8 ]
void destroy ( X * p , Arena & arena ) { p ->~ X (); //まず、デストラクタを明示的に呼び出します。arena.deallocate ( p ); // 次に、デアロケータ関数を直接呼び出します。}これは、プログラムから次のように呼び出されます。
Arena arena ; X * p = new ( arena ) X ; // ... destroy ( p , arena );後者は、デストラクタ呼び出しと削除関数呼び出しをプログラムに書き込むだけです。[ 7 ] [ 14 ]
Arena arena ; X * p = new ( arena ) X ; // ... p ->~ X (); // まず、デストラクタを明示的に呼び出します。operator delete ( p , arena ); // 次に、operator delete(void*, Arena&) を介してデロケータ関数を間接的に呼び出します。deleteよくあるエラーは、オブジェクトに対して式を使用しようとすることですdelete。これにより、間違ったoperator delete関数が呼び出されます。Dewhurst は、このエラーを回避するための 2 つの戦略を推奨しています。1 つ目は、カスタム アロケータが標準ライブラリのグローバルな非配置アロケータに依存するようにしoperator new、C++ ライブラリのメモリ管理を単純にラップするだけのものにすることです。2 つ目は、個々のクラスに対して関数を作成しnew、delete配置構文を使用するのではなく、クラス関数メンバーを介してメモリ管理をカスタマイズすることです。[ 14 ]
配置は、メモリ割り当てが失敗したソースコードnewのファイル名と行番号をプログラムが出力できるようにする、簡単なデバッグツールとしても使用できます。これには標準ライブラリヘッダーを含める必要はありませんが、4 つの配置関数と、式で使用されるキーワードのマクロ置換を宣言するヘッダーを含める必要があります。たとえば、そのようなヘッダーには次の内容が含まれます。[ 9 ] [ 15 ]<new>newnew
#if defined(DEBUG_NEW) void * operator new ( size_t size , const char * file , int line ); void * operator new []( size_t size , const char * file , int line ); void operator delete ( void * p , const char * file , int line ); void operator delete []( void * p , const char * file , int line ); #define New new(__FILE__, __LINE__) #else #define New new #endifこれは次のようなプログラムで使用されます。[ 9 ] [ 15 ]
X * p =新しいX ;例外が発生した場合、カスタムで作成された配置new関数は、提供されたファイルと行番号の情報を使用して処理を行います。例: [ 9 ] [ 15 ]
import std ;using std :: bad_alloc ; using std :: nothrow ;class FailedAllocationException : public bad_alloc { private : const char * file ; int line ; public : FailedAllocationException ( const char * file , int line ) : bad_alloc (), file { file }, line { line } {} };void * operator new ( size_t size , const char * file , int line ) { if ( void * p = :: operator new ( size , nothrow )) { return p ; } throw FailedAllocationException ( file , line ); }C++26以降、配置はconstexprnewコンテキストで使用できます。
import std ;template < typename T > class Box { private : alignas ( T ) unsigned char storage [ sizeof ( T )]; T * ptr = nullptr ;constexpr void destroy () { if ( active ) { std :: destroy_at ( ptr ); // デストラクタを手動で呼び出すptr = nullptr ; } } public : constexpr Box () = default ;constexpr ~ Box () { destroy (); }template < typename ... Args > constexpr T & emplace ( Args && ... args ) { destroy (); T * p = new ( static_cast < void *> ( storage )) T ( std :: forward < Args > ( args )...); return * ptr ; }constexpr T & get () { return * ptr ; } };上記のとおり、配置式はありません。式を使用して配置関数をdelete呼び出すことはできません。[ 11 ] [ 16 ]operator deletedelete
配置delete関数は配置式から呼び出されますnew。特に、オブジェクトのコンストラクタが例外をスローした場合に呼び出されます。このような状況では、プログラムがメモリリークを起こさないようにするために、配置削除関数が呼び出されます。配置式は、newまず配置operator new関数を呼び出し、次にアロケータ関数から返された生のストレージに対してオブジェクトのコンストラクタを呼び出します。コンストラクタが例外をスローした場合、配置new式を実行したコードに例外を伝播する前にそのストレージを解放する必要があり、それが配置関数の目的ですdelete。[ 2 ] [ 4 ] [ 11 ] [ 16 ]
呼び出される配置削除関数は、new配置式によって呼び出された配置関数と一致しますnew。したがって、たとえば、次のコードが実行されると、delete呼び出される配置関数は次のようになりますoperator delete(void*, const Arena&)。[ 2 ] [ 11 ] [ 16 ]
import std ;std :: bad_allocを使用します。class Arena { // ... };class X { public : X () { throw bad_alloc ( "不正な割り当て" ); } };void * operator new ( size_t n , const Arena & a ) { std :: println ( "配置 new が呼び出されました。" ); }void operator delete ( void * p , const Arena & a ) { std :: println ( "配置削除が呼び出されました。" ); }int main ( ) { Arena arena ; try { X * p = new ( arena ) X ; } catch ( const bad_alloc & e ) { std :: println ( "Exception caught: {}" , e.what ( )); } return 0 ; }そのため、ポインタ配置delete関数は標準ライブラリでは何もしないものとして定義されています。ポインタ配置new関数はストレージを割り当てないため、オブジェクトのコンストラクタが例外をスローした場合でも、解放されるストレージはありません。[ 17 ] [ 11 ]
対応する配置削除関数が存在しない場合、配置式内のコンストラクタによって例外がスローされたときに、解放関数は呼び出されませんnew。また、配置をサポートしていない(古い)C++実装もいくつかありますdelete(配置は、例外をスローするアロケータ関数と同様に、C++が標準化されたときに追加されたものです)。このような状況では、カスタムアロケータを使用して割り当てているときにコンストラクタによって例外がスローされると、メモリリークが発生します。(古いC++実装の場合、配置式がないnew場合でもメモリリークが発生します。)[ 4 ] [ 16 ]
配置new式はセキュリティ攻撃に対して脆弱です。2011年にKunduとBertino [ 17 ]は配置に対するいくつかの攻撃を実証しましたnew。攻撃には、バッファオーバーフロー攻撃、オブジェクトオーバーフロー、選択的スタックガードオーバーライド、仮想ポインタの偽装、メモリアライメントのずれ攻撃などがあります。2015年にGCCは[ 17 ]の調査結果に基づいてパッチ[ 18 ]をリリースしました。