詳解、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 Tnew T() の略記だと思っている人がいるが、それは間違い。new T() がデフォルトコンストラクタによるオブジェクトの構築を必ず行うのに対して、new T では、T が POD 型である場合にはコンストラクタが呼び出されない。ちょうど、int 型のローカル変数を初期化せずに定義したときの値が不定になるような感じだ。
配列を new する際のそれぞれのオブジェクトの構築は、new T のそれと同じである。intchar の配列を動的に確保する際には、十分注意しよう。

“普通”の 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)。