詳解、new 演算子
new
演算子に対して以下のような疑問はないだろうか?
new
演算子とoperator new
の違いがよく分からない。- プレイスメント (配置構文)
new
がよく分からない。 - クラスのメンバに
operator new
を用意するというのがどういうことなのかよく分からない。 - デバッグ用にリークを検知するメモリ割り当て機構を使いたいがよく分からない。
- ヒープの断片化で性能が劣化しているから Boost Pool Library を試してみたいがよく分からない((Boost Pool Library の API には
new
演算子関連の話は現れないが、知っておくとイメージが掴みやすくていいと思う。))。
僕も、断片的に知ってはいたが気になっていろいろと調べてみたので、忘れないようにメモしておく。
new
式の構文
C++ 標準*1によれば、new
式の構文は以下のようになっている:
new-expression: ::opt new new-placementopt new-type-id new-initializeropt ::opt new new-placementopt ( type-id ) new-initializeropt new-placement: ( expression-list ) expression-list: 略: カンマ区切りの式の並び、つまりは引数リスト new-type-id: type-specifier-seq new-declaratoropt type-specifier-seq: 略: 型の名前や CV-修飾子の繰り返し (繰り返しなのはconst int
のようなもののため) new-declarator: ptr-operator new-declaratoropt direct-new-declarator ptr-operator: 略: ポインタであることを示す*
など (多重ポインタやメンバポインタもあるので結構複雑) direct-new-declarator: [ expression ] direct-new-declarator [ constant-expression ] expression: 略: いわゆる“式”、ここでは配列の添え字になる → 整数に評価される式でないといけない constant-expression: 略: “定数式” (多次元配列では、最初の添え字以外は定数でなければならない) type-id: 略: 型の表現の全て (関数ポインタなどは new-type-id の範疇に収まらない) new-initializer: ( expression-listopt )
ややこしいが、要するにこういうことだ:
new(
プレイスメント引数)
型名(
コンストラクタ引数)
「(
プレイスメント引数)
」の部分をプレイスメントと呼ぶが、これはなくてもいい。「(
コンストラクタ引数)
」も省略できる。new
の前に ::
を付けてもいい。関数ポインタのような複雑な型名は、括弧で囲む必要がある。
割当と構築
C++ でのオブジェクトの生成はメモリの割当とオブジェクトの構築からなるが、new
式の評価ではまさにこの2つが実行される。「new(p, q) T(a, b)
」という new
式による T
型のオブジェクトの生成は、擬似コードで書くとこんな感じ:
/* 割当 */ T* const this( operator new (sizeof(T), p, q) ); /* 構築 */ this->T::T(a, b);
もちろん、this
という変数を定義・初期化したりコンストラクタ T::T
を明示的に呼び出すことはできないが、そこは擬似コードなので許してほしい。言いたいことは:
- メモリの割当は
operator new
という名前の関数で行われる。 operator new
関数にはプレイスメント引数が渡されるが、その前にsizeof(T)
が第1引数として補われる。
それと、配列を生成する場合の動作は少し違っていて、こんな感じになっている:
/* 割当 */ char* const rawMemory( operator new[] (sizeof(T) * 要素数 + オーバヘッド, p, q) ); T* const array( reinterpret_cast<T*>(rawMemory + オーバヘッド) ); /* 構築 */ for (int i = 0; i < 要素数; ++i) { T* const this( array + i ); this->T::T(); } // ただし、T が POD 型である場合には、「構築」のステップは行われない。
このように、一括して割り当てられたメモリの上に、要素数分のオブジェクトが順次構築される。また、以下の点には注意する必要がある:
- 配列の場合、メモリ割当には、
operator new
ではなくoperator new[]
という名前の関数が使われる。 - 配列の要素の構築では、コンストラクタ引数は指定できない。
operator new[]
関数には、配列に必要なサイズにオーバヘッドが加えられたサイズが要求される。- オーバヘッドは、配列の“前”にある (
operator new[]
関数の返却値とnew
式の結果が指すアドレスは異なる)。 - オーバヘッドのサイズは、
new
式ごとに異なる可能性があるだけでなく、同じnew
式でも評価のたびごとに異なる可能性がある。
new T()
と new T
の微妙な違い
new T
を new T()
の略記だと思っている人がいるが、それは間違い。new T()
がデフォルトコンストラクタによるオブジェクトの構築を必ず行うのに対して、new T
では、T
が POD 型である場合にはコンストラクタが呼び出されない。ちょうど、int
型のローカル変数を初期化せずに定義したときの値が不定になるような感じだ。
配列を new
する際のそれぞれのオブジェクトの構築は、new T
のそれと同じである。int
や char
の配列を動的に確保する際には、十分注意しよう。
“普通”の operator new
関数
メモリの割当は operator new (sizeof(型), プレイスメント引数)
という関数呼び出しで行われるわけだが、では、この operator new
という名前の関数はどこにあるのか。結論から言ってしまうと、そういう関数を自分で用意しなければならない。だが、以下の2つの関数は、C++ の処理系が提供することになっているので定義しなくても利用できる:
void* operator new (std::size_t) throw(std::bad_alloc); void* operator new[] (std::size_t) throw(std::bad_alloc);
これらの関数は全ての翻訳単位において暗黙に宣言されているため、以下のような“普通”の new
式はいつでも使える:
new T; new T(); new T(a, b); new T[length];
ちなみに、この“普通”の operator new
/new[]
関数は、自分で定義してもよい (当然、この場合には、処理系はこれらの関数を生成しなくなる)。このため、デバッグ用にリークを検知するメモリ割り当て機構を使いたい場合には、以下のような debug_new.cpp をソースに加えればよい:
#ifndef NDEBUG void* operator new (std::size_t const size) throw(std::bad_alloc) { // リークを検知するメモリ割り当て機構を使って、size バイトのメモリを割り当てる。 } #endif
例外を投げない operator new
関数
C++ の処理系は、以下のような operator new
/new[]
関数も提供することになっている:
#include <new> void* operator new (std::size_t, const std::nothrow_t&) throw(); void* operator new[] (std::size_t, const std::nothrow_t&) throw();
“普通”の operator new
とは異なり、<new>
ヘッダにある宣言をインクルードしなければならない。std::nothrow_t
は、グローバル変数である std::nothrow
を定義するためだけの型で、オーバロードのためだけにある。ちょうど、++
演算子の前置・後置を区別するためだけに operator ++
に int
型の引数を持たせるような感じだ。new
式としての使い方は、こんな感じ:
new(std::nothrow) T; new(std::nothrow) T(); new(std::nothrow) T(a, b); new(std::nothrow) T[length];
これらは“普通”の new
とほとんど同じだが、メモリの割当に失敗した際に null ポインタを返す点のみ異なる (“普通”の new
は、メモリの割当に失敗すると std::bad_alloc
例外を送出する)。
プレイスメント new
標準 C++ は、もう1組、以下のような operator new
/new[]
関数を提供する:
#include <new> void* operator new (std::size_t, void*) throw(); void* operator new[] (std::size_t, void*) throw();
例外を投げない new
と同じように、<new>
ヘッダにある宣言をインクルードしなければならない。こいつらはやる気のない関数で、引数に渡された void*
の値をただ返すだけだ。つまり、こんな感じで使う:
void* const memory( /* 何らかの方法で適当なサイズのメモリを割り当てる */ ); new(memory) T; // T(), T(a, b), T[length] などでも同様
つまり、メモリの割当は何らかの方法で済んでいてオブジェクトの構築だけを行いたい場合に使用する。メモリ空間への配置 (placement) を明示的に指定する new
なので、これをプレイスメント new
という((プレイスメント引数を伴う new
は全部“プレイスメント new
”ではあるんですが、まあ、“狭義のプレイスメント new
”といったところです。))。
クラスごとの operator new
関数
new
式の評価に際して operator new
/new[]
関数が検索される際には、最初にクラススコープでの名前解決が試みられる。すなわち、new T
であれば、T::operator new (std::size_t)
, 基底クラス::operator new (std::size_t)
, 基底クラスの基底クラス::operator new (std::size_t), ……,
::operator new (std::size_t)
を順に探して最初に見つかったものが使用される。これにより、特定のクラス (とその派生クラス) にのみ特別のメモリ割当機構を使用するといったことが可能になる。具体的には、このように書く:
class some_class { public: static void* operator new (std::size_t const size) { // このクラス専用のメモリ割当を行う。 } static void* operator new[] (std::size_t const size) { /* このクラスの配列専用のメモリ割当を行う */ } // 必要に応じて、他のオーバロードも自由に書ける。 };
その位置付けから明らかであるが、operator new
/new[]
関数は static
メンバ関数でなければならない。そのため、static
と書かなくてもコンパイラは勝手にこれを static
にする。はっきり言って余計なお世話だが、そういうものなので仕方がない。悪いことは言わないので、static
は書くようにしよう。
クラスごとの operator new
関数の抑制
独自の operator new
/new[]
関数を持つクラスであっても、「::new T
」のように new
演算子の前に ::
を付ければ、大域に定義された operator new
を使うようになる。
ちなみに、クラスのメンバでもなく大域でもない operator new
/new[]
関数を使わせる手だてはない。というか、そのような operator new
/new[]
関数は定義できない。クラスメンバでない場合は、operator new
/new[]
関数を名前空間の中に宣言することはできないのだ((Visual C++ 2005 では、非メンバ関数の operator new
/new[]
を名前空間の中で定義してもコンパイルエラーにはならない。)):
- C++ 標準 ― 3.7.3.1 Allocation functions
- An allocation function shall be a class member function or a global function; a program is ill-formed if an allocation function is declared in a namespace scope other than global scope or declared static in global scope.
*1:ISO/IEC 14882:2003(E)。