C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。
この章の概要です。
第19章、第35章で、演算子のオーバーロードについて取り上げましたが、これらの章では、new演算子と delete演算子をオーバーロードすることについては取り上げませんでした。この章では、この話題について解説します。
まず、new演算子や delete演算子が何をしているのかについて、確認しておきましょう。大体のことは、第14章で一度説明していますから、まずはそちらを参照してください。
重要なのは、new演算子は大きく分けて、2つの処理を以下の順序でおこなう統合的な機能だということです。
同様に、delete演算子は、次の2つの処理をこの順序で行います。
さて、この中で、オブジェクトの生成と解体をするという部分は変更できません。生成はコンストラクタが、解体はデストラクタが行っているのですから、それらの内容を都合に合わせて書けば良いです。
一方で、メモリ領域の確保や解放📘の部分は変更できます。メモリ領域の確保は operator new(配列なら operator new[])という関数が、解放は operator delete(配列なら operator 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();
前の項で紹介した 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
Visual Studio 2017 では、例外仕様に関する警告が出ますが、これは、Visual Studio では、throw() 以外の形の例外仕様は、機能しないことを伝えるものです。
このサンプルのように、実のところ、メモリ確保や解放処理そのものを変えたいわけではない場合は、malloc関数や free関数を使って、確保・解放をおこなうようにします。
operator new が確保する領域は、どんな型のために使われても問題がないアラインメント📘でなければならないという要求があります。malloc関数はこの要求に応えられます。
また、前の項で触れたとおり、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()
{
* p1 = new MyClass();
MyClassdelete p1;
* p2 = new MyClass[1000];
MyClassdelete [] 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 を呼べます。
この章の冒頭で取り上げたように、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)];
* p = new(buffer) MyClass();
MyClass->~MyClass();
p}
実行結果:
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 を定義してください。
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
![]() |
管理者情報 | プライバシーポリシー |