C++編【言語解説】 第36章 operator new/delete

先頭へ戻る

この章の概要

この章の概要です。


new/delete をカスタマイズする

第19章第35章で、演算子のオーバーロードについて取り上げましたが、これらの章では、new演算子と delete演算子をオーバーロードすることについては取り上げませんでした。この章では、この話題について解説します。

まず、new演算子や delete演算子が何をしているのかについて、確認しておきましょう。大体のことは、第14章で一度説明していますから、まずはそちらを参照してください。

重要なのは、new演算子は大きく分けて、2つの処理を以下の順序で行う統合的な機能だということです。

  1. メモリ領域を確保する。
  2. オブジェクトを生成する(コンストラクタの呼び出し)

同様に、delete演算子は、次の2つの処理をこの順序で行います。

  1. オブジェクトを解体する(デストラクタの呼び出し)
  2. メモリ領域を解放する。

さて、この中で、オブジェクトの生成と解体をするという部分は変更できません。生成はコンストラクタが、解体はデストラクタが行っているのですから、それらの内容を都合に合わせて書けば良いです。

一方で、メモリ領域の確保や解放の部分は変更できます。メモリ領域の確保は operator new(配列なら operator new[])という関数が、解放は operator delete(配列なら operator delete[])という関数が担っているので、これらをカスタマイズすれば良いということになります。

operator new/delete

デフォルトの new演算子が、メモリ領域を確保する際には、標準で用意されている operator new という関数が呼び出されます。同様に、デフォルトの delete演算子は、メモリ領域を解放する際には、標準で用意されている operator delete を呼び出します。それぞれ、(特定の名前空間に含まれない)グローバルな場所に、以下のように定義されています。

void* operator new(std::size_t) throw(std::bad_alloc);
void* operator new[](std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
void operator delete[](void*) throw();

これらの関数は、同じ形式で、グローバル名前空間に新たな定義を書くと、それで置換できます。つまり、デフォルトの operator new/delete を上書きして、独自の operator new/delete に置き換えてしまえます。

new演算子には、情報を付加する構文があり、例えば、次のように書けます。これは、配置構文と呼ばれています。

型名* p = new(std::nothrow) 型名();

newキーワードの直後に ( ) を使って情報を付加できます。「std::nothrow」は、標準で定義されている std::nothrow_t型の定数で、<new> という標準ヘッダをインクルードすると使用できます。

「new(std::nothrow)」という形で new演算子を使用する形は、標準で定義されているもので、この使い方をすると、メモリ確保に失敗したときに、std::bad_alloc例外を送出するのではなく、ヌルポインタを返す動作になります。ただし、この使い方は推奨されておらず、C++ が規格化される前の古い書き方との互換性のために残されているものです。新規のプログラムでは使うべきではありません

new(std::nothrow) を指定したからといって、必ずしも例外が送出されなくなる訳ではありません。この指定はあくまでも、(この章の冒頭で説明した)new演算子が行う2つの処理のうちの、「メモリ領域の確保」の部分に対するものです。「オブジェクトを生成する」のところで例外が送出されれば、結局、その例外は new演算子を呼び出している箇所にまでやってきます。

このように、配置構文の new演算子を使用した場合、メモリ領域を確保する際に呼び出される operator new の第2引数以降が変化します。std::nothrow を使う場合だと、以下のようになります。

void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();
void operator delete(void*, const std::nothrow_t&) throw();
void operator delete[](void*, const std::nothrow_t&) throw();

operator delete の方も書きましたが、operator new と operator delete(と、それぞれの配列版)には、必ず対応関係が取られています。operator new/delete はそれぞれ第1引数は std::size_t型、void*型で固定されており、第2引数以降が両者の対応関係を表現しています

なお、std::nothrow を受け取る上記の形の operator new/delete も、ユーザー独自の関数を定義して置換できます

引数が1つだけのデフォルトの operator delete 以外の operator delete については、直接的に呼び出すことができません。new演算子に配置構文があるのと違って、delete演算子にはそのようなものはありません。ではなぜ、呼び出せもしない operator delete が定義されているかというと、new演算子がオブジェクトの生成を行っている最中に失敗したとき、既に確保してしまったメモリ領域を解放するために自動的に使われるからです。

この章の冒頭で確認したように、new演算子は、メモリ領域を確保してから、オブジェクトの生成を行います。オブジェクトの生成というのは、つまりはコンストラクタの呼び出しですが、この処理の途中で例外が送出されるようなことがあると、new演算子の処理全体としては「失敗」したことになります。メモリ領域が確保されたままになってはいけませんから、new演算子の処理から戻る前に、解放処理を行いますが、このとき、メモリ領域を確保する際に使った operator new に対応する operator delete が呼び出されるという訳です。

標準で定義されている operator new/delete は、もう1タイプあります。

void* operator new(std::size_t, void*) throw();
void* operator new[](std::size_t, void*) throw();
void operator delete(void*, void*) throw();
void operator delete[](void*, void*) throw();

この形式の operator new は、指定の場所にオブジェクトを生成させたいときに使います。std::nothrow の場合と同じく、new に続く ( ) にポインタを指定すると、この形式の operator new が使用されます。そして、オブジェクトの生成が失敗すると、この形式の operator delete が使用されます。

この形式の operator new/delete は、置換できません。ただし、後の項で取り上げるように、クラス専用の opertator new/delete として定義できます。この使い方は、一般的に placement new と呼ばれています。

まとめると、標準で定義されている operator new/delete は以下の通りです。なお、これらはすべて、<new> という名前の標準ヘッダで定義されています。

// 確保する大きさの指定のみ。デフォルトのタイプ。置換できる。
void* operator new(std::size_t) throw(std::bad_alloc);
void* operator new[](std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
void operator delete[](void*) throw();

// メモリ確保に失敗したとき、例外を送出しないタイプ。new(std::nothrow) で使用。置換できる。
void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();
void operator delete(void*, const std::nothrow_t&) throw();
void operator delete[](void*, const std::nothrow_t&) throw();

// 確保済みのメモリ領域を使用するタイプ。placement_new。置換できない。
void* operator new(std::size_t, void*) throw();
void* operator new[](std::size_t, void*) throw();
void operator delete(void*, void*) throw();
void operator delete[](void*, void*) throw();

C++11 (例外指定の変更)

C++11 になって、従来からある例外指定の機能が非推奨になったため(第32章)、operator new/delete の例外指定のうち、throw(std::bad_alloc) は無くなり、throw() は noexcept に改められました。

まとめると、以下のようになります。

// 確保する大きさの指定のみ。デフォルトのタイプ。置換できる。
void* operator new(std::size_t);
void* operator new[](std::size_t);
void operator delete(void*) noexcept;
void operator delete[](void*) noexcept;

// メモリ確保に失敗したとき、例外を送出しないタイプ。new(std::nothrow) で使用。置換できる。
void* operator new(std::size_t, const std::nothrow_t&) noexcept;
void* operator new[](std::size_t, const std::nothrow_t&) noexcept;
void operator delete(void*, const std::nothrow_t&) noexcept;
void operator delete[](void*, const std::nothrow_t&) noexcept;

// 確保済みのメモリ領域を使用するタイプ。placement_new。置換できない。
void* operator new(std::size_t, void*) noexcept;
void* operator new[](std::size_t, void*) noexcept;
void operator delete(void*, void*) noexcept;
void operator delete[](void*, void*) noexcept;

VisualStudio 2015/2017 では、throw(std::bad_alloc) は無くなっていますが、throw() はそのままのようです。意味としては変わりありません。

C++14 (大きさ指定付きの operator delete)

C++14 で、以下の operator delete が追加されました。

void operator delete(void*, std::size_t) noexcept;
void operator delete[](void*, std::size_t) noexcept;
void operator delete(void*, std::size_t, const std::nothrow_t&) noexcept;
void operator delete[](void*, std::size_t, const std::nothrow_t&) noexcept;

第2引数の std::size_t のところに、解放対象となるメモリ領域の大きさが渡されます。ここで渡される値は、operator new の第1引数に渡される大きさと一致します。

従来からのルールでは、operator new と operator delete には、2つ目以降の引数の一致による対応関係が取られていましたが、今回の operator delete の追加によって、少し関係性が崩れました。呼び出す operator delete を決める際には、今回追加された std::size_t型の引数があるものと無いものとが両方とも定義されていたら、std::size_t型の引数ががあるものを優先して使います。そのため、大きさが必要であるならば、std::size_t型の引数がある方だけを用意すれば良いです。

#include <iostream>
#include <new>

void* operator new(std::size_t size)
{
    std::cout << "new: " << size << std::endl;
    return std::malloc(size);
}

void* operator new[](std::size_t size)
{
    std::cout << "new[]: " << size << std::endl;
    return std::malloc(size);
}

void operator delete(void* p, std::size_t size) noexcept
{
    std::cout << "delete: " << size << std::endl;
    std::free(p);
}

void operator delete[](void* p, std::size_t size) noexcept
{
    std::cout << "delete[]: " << size << std::endl;
    std::free(p);
}

struct MyData {
    ~MyData() {}
};

int main()
{
    int* p1 = new int(0);
    delete p1;

    int* p2 = new int[1000];
    delete [] p2;

    MyData* p3 = new MyData[1000];
    delete [] p3;
}

実行結果:

new: 4
delete: 4
new[]: 4000
new[]: 1004
delete[]: 1004

clang 5.0.0 の場合は、コンパイラオプション「-fsized-deallocation」が必要です。

VisualStudio 2015/2017、clang 5.0.0 で確認すると、変数p2 に対する delete のログが出力されませんでした。また、MyData構造体から、デストラクタの定義を消すと、変数p3 に対する delete のログも出力されなくなります。明示的なデストラクタが必要なようです。

グローバルな operator new/delete の置換

前の項で紹介した operator new/delete のうち、引数が1個だけのバージョンと、std::nothrow_t を伴うバージョンは、ユーザーが定義した関数で置換できます。

試しに、new と delete のログを出力するようにしてみましょう。

#include <iostream>
#include <new>
#include <cstdlib>

void* operator new(std::size_t size) throw(std::bad_alloc)
{
    void* const p = std::malloc(size);
    std::cout << "new: " << p << std::endl;
    return p;
}

void* operator new[](std::size_t size) throw(std::bad_alloc)
{
    void* const p = std::malloc(size);
    std::cout << "new[]: " << p << std::endl;
    return p;
}

void operator delete(void* p) throw()
{
    std::cout << "delete: " << p << std::endl;
    std::free(p);
}

void operator delete[](void* p) throw()
{
    std::cout << "delete[]: " << p << std::endl;
    std::free(p);
}

int main()
{
    int* p1 = new int(0);
    delete p1;

    int* p2 = new int[1000];
    delete[] p2;
}

実行結果:

new: 005A9E40
delete: 005A9E40
new[]: 005AF200
delete[]: 005AF200

VisualStudio 2015/2017 では、例外仕様に関する警告が出ますが、これは、VisualStudio では、throw() 以外の形の例外仕様は、機能しないことを伝えるものです。

このサンプルのように、実のところ、メモリ確保や解放処理そのものを変えたい訳ではない場合は、malloc関数や free関数を使って、確保・解放を行うようにします

operator new が確保する領域は、どんな型のために使われても問題がないアラインメントでなければならないという要求があります。malloc関数はこの要求に応えられます。

また、前の項で触れた通り、operator new/delete には対応関係があります。一方を置換するのなら、必ず他方に対応するものを置換するようにしてください

以上が、グローバルに operator new/delete を置換する方法ですが、実戦的に言えば、影響範囲が広すぎるため、グローバルに置換することはできません。特に、他人が書いたソースコードを組み合わせてプログラムを作る場合、他人が書いた部分にまで影響を与えることになるため、本当に正しく動作するかどうか、確証を持つことが難しくなります。

そこで、次の項で取り上げるように、特定のクラスをインスタンス化するときに限定して、メモリ確保・解放の方法を書き換える方法が使えます。

クラス専用の operator new/delete

operator new/delete は、クラスの staticメンバ関数として定義することが可能です。これは、(グローバルな operator new/delete を)置換している訳ではなく、より狭いスコープに新たな関数を追加しています。そのため、グローバルな operator new/delete を隠蔽することに注意してください

なお、staticメンバ関数でなければならないですが、static指定子は付けても付けなくても同じことになります

クラスの staticメンバ関数としての operator new/delete(以降、クラス専用の operator new/delete と記述します)は、そのクラスのオブジェクトを new演算子によって生成するときに使われます。operator delete の方は、new演算子が途中で失敗したときに呼び出されます。

以下のサンプルプログラムで動作を確認してみましょう。

#include <iostream>
#include <new>

class MyClass {
public:
    ~MyClass() {}

    static void* operator new(std::size_t size) throw(std::bad_alloc)
    {
        void* const p = ::operator new(size);
        std::cout << "new: " << p << std::endl;
        return p;
    }

    static void* operator new[](std::size_t size) throw(std::bad_alloc)
    {
        void* const p = ::operator new[](size);
        std::cout << "new[]: " << p << std::endl;
        return p;
    }

    static void operator delete(void* p) throw()
    {
        std::cout << "delete: " << p << std::endl;
        ::operator delete(p);
    }

    static void operator delete[](void* p) throw()
    {
        std::cout << "delete[]: " << p << std::endl;
        ::operator delete[](p);
    }
};

int main()
{
    MyClass* p1 = new MyClass();
    delete p1;

    MyClass* p2 = new MyClass[1000];
    delete [] p2;
}

実行結果:

new: 00B59E40
delete: 00B59E40
new[]: 00B5F200
delete[]: 00B5F204

このサンプルプログラムのように、メモリ確保、解放の処理そのものを変更する必要がないのなら、グローバルな方の operator new/delete を呼び出すように実装してください。そうすることで、「0バイトの確保要求でも有効なメモリアドレスを返さなければならない」だとか、「ヌルポインタに対する delete は何も起こらない」などといった、標準の new や delete が持っている基本ルールが自動的に守られることになります。

クラス専用の operator new/delete の実装コードから、グローバルな方の operator new/delete を呼び出すには、「::operator new」のように、グローバル名前空間に属していることを明示的に指定してください。

クラス専用の operator new/delete は、グローバルな方の operator new/delete を隠蔽することに注意してください。先ほどのサンプルプログラムのように、1種類の operator new/delete だけを定義したとしても、引数が異なる他のバージョンのグローバルな operator new/delete も隠蔽されてしまいます。

そのため、クラス専用の operator new/delete を1種類でも定義するのなら、全種類とも定義した方が良いです。必要性があると考えて定義したもの以外は、デフォルトの動作を維持したいので、単にグローバルな operator new/delete を呼ぶように実装すれば良いでしょう。例えば、以下のようになります。

class MyClass {
public:
    ~MyClass() {}

    static void* operator new(std::size_t size) throw(std::bad_alloc)
    {
        void* const p = ::operator new(size);
        std::cout << "new: " << p << std::endl;
        return p;
    }

    static void* operator new[](std::size_t size) throw(std::bad_alloc)
    {
        void* const p = ::operator new[](size);
        std::cout << "new[]: " << p << std::endl;
        return p;
    }

    static void operator delete(void* p) throw()
    {
        std::cout << "delete: " << p << std::endl;
        ::operator delete(p);
    }

    static void operator delete[](void* p) throw()
    {
        std::cout << "delete[]: " << p << std::endl;
        ::operator delete[](p);
    }

    static void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
    {
        return ::operator new(size, nt);
    }
    static void* operator new[](std::size_t size, const std::nothrow_t& nt) throw()
    {
        return ::operator new[](size, nt);
    }
    static void operator delete(void* p, const std::nothrow_t& nt) throw()
    {
        ::operator delete(p, nt);
    }
    static void operator delete[](void* p, const std::nothrow_t& nt) throw()
    {
        ::operator delete[](p, nt);
    }

    static void* operator new(std::size_t size, void* where) throw()
    {
        return ::operator new(size, where);
    }
    static void* operator new[](std::size_t size, void* where) throw()
    {
        return ::operator new[](size, where);
    }
    static void operator delete(void* p, void* where) throw()
    {
        ::operator delete(p, where);
    }
    static void operator delete[](void* p, void* where) throw()
    {
        ::operator delete[](p, where);
    }
};

あるいは、new演算子や delete演算子を使用するときに、「::new」「::delete」のように、グローバル名前空間であることを明示すると、クラス専用の operator new/delete が定義されていても、グローバルな方の operator new/delete を呼べます


placement new(配置new、配置構文new)

この章の冒頭で取り上げたように、new演算子を使うと、メモリ領域の確保と、オブジェクトの生成が行われますが、「メモリ領域の確保」の部分をカスタマイズできるのなら、「確保しない」というカスタマイズも考えられます。もちろん、メモリ領域を使わずにオブジェクトを生成できませんが、事前にどこかに領域が確保済みであれば、「確保済みの領域を使わせるように指示して、新たな確保は行わない」という手段は取れます。

このような使い方をする new のことを、placement new(配置new、配置構文new)と呼びます。

本来は、new演算子を「new(~) 型」のように使って、operator new へ追加の引数を与える機能を指して、placement new と言います。つまり、追加の引数が、どこかのメモリ領域を指すポインタであっても、それ以外の無関係な情報であっても、とにかく追加の引数があれば placement new と呼ぶこともあります。

これを実現するために、既に取り上げていますが、以下の operator new/delete がデフォルトで用意されています。

void* operator new(std::size_t, void*) throw();
void* operator new[](std::size_t, void*) throw();
void operator delete(void*, void*) throw();
void operator delete[](void*, void*) throw();

この operator new/delete を使わせるには、new の配置構文を使い、new に続く ( ) に、メモリアドレスを指定します。試してみましょう。

#include <iostream>
#include <new>

class MyClass {
public:
    MyClass()
    {
        std::cout << "constructor" << std::endl;
    }

    ~MyClass()
    {
        std::cout << "destructor" << std::endl;
    }
};

int main()
{
    char buffer[sizeof(MyClass)];

    MyClass* p = new(buffer) MyClass();
    p->~MyClass();
}

実行結果:

constructor
destructor

デフォルトの挙動で構わないのであれば、デフォルトで用意されているグローバルな operator new/delete を使うだけなので、独自の関数で置換する必要も、クラス専用の関数を定義する必要もありません。

placement new ではメモリ領域の確保を行っていないので、対応する delete演算子は必要ありません。しかし、それだと new演算子が行う「オブジェクトの生成」に対応して行われるべき「オブジェクトの解体」が行われなくなってしまうので、そこだけ何とかしなければなりません

そこで、「p->~MyClass()」のようにして、デストラクタだけを明示的に呼びます

デフォルトとは異なる placement new が必要であれば、クラス専用の operator new/delete を定義します。グローバルな placement new は置換できません。前の項にも書いたように、クラス専用の operator new/delete を1タイプでも定義するのなら、全タイプとも定義してください。

placement new を利用して実装される仕組みにアロケータがあります。標準ライブラリにも、アロケータが定義されており、STLコンテナが使用するメモリ領域を管理する仕組みとして使われています。詳細については、【標準ライブラリ】第32章を参照してください。


練習問題

問題① 配置構文new が失敗したとき、operator delete が呼び出されていることを確認するプログラムを作成してください。

問題② 確保済みの領域のメモリアドレスと、メモリ確保時に出力する任意の文字列を渡せるような placement new を定義してください。


解答ページはこちら

参考リンク



更新履歴

'2018/7/13 サイト全体で表記を統一(「静的メンバ」-->「staticメンバ」)

'2018/4/5 VisualStudio 2013 の対応終了。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。
Xcode 8.3.3 を clang 5.0.0 に置き換え。

≪さらに古い更新履歴を展開する≫



前の章へ (第35章 非メンバの演算子オーバーロード)

C++編のトップページへ

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー