LoginSignup
42
36

More than 5 years have passed since last update.

new/delete 演算子のオーバーロード

Last updated at Posted at 2018-07-27

はじめに

C++オーバーロード大全では、たくさんのストックを頂きありがとうございました。
この記事は、上の記事で解説することを避けたCreate/Destroy ObjectおよびCreate/Destroy Objects、すなわちnew/delete演算子とnew[]/delete[]演算子について解説したいと思います。

なお、規格の参照には、C++11にはn3337を、C++14にはn3936を使用しました。正規の規格書ではないのでご了承ください。

Create/Destroy Object

// usual new/delete
void* operator new(std::size_t) ; // ...(A)
void operator delete(void*) noexcept ; // ...(B)
void operator delete(void*, std::size_t) noexcept ; // ...(C)

// placement new/delete
void* operator new(std::size_t, Args...) ; // ...(D)
void operator delete(void*, Args...) noexcept ; // ...(E)

オブジェクト構築/破壊演算子。規格ではallocation or deallocation functionと呼ばれているので、確保/解放関数としてもいいですね。まあ、そんな名前では通常呼ばれず、ふつうは単にnew/deleteと呼ばれます。この演算子は他の演算子と違い、引数や戻り値に厳密な制限があります。また、この演算子は注意深くオーバーロードする必要があります。usual(non-placement) new/delete、placement new/delete、クラススコープに定義される場合、グローバルスコープに定義される場合というように場合分けして説明していきたいと思います。

クラススコープに定義される場合

usual new/delete

usual new/delete演算子とは、(placement new/deleteと比較して)通常のnew/delete演算子のことです。つまり、以下のような文脈で呼ばれるnew/delete演算子のことを言います。

sth* p = new sth(); // usual new
delete p; // usual delete

usual new演算子はシグニチャがvoid* operator new(std::size_t)でなければなりません。1
usual delete演算子はシグニチャがvoid operator delete(void*)あるいはvoid operator delete(void*, std::size_t)でなければなりません。2

// usual new/delete
void* operator new(std::size_t) ; // ...(A)
void operator delete(void*) noexcept ; // ...(B)
void operator delete(void*, std::size_t) noexcept ; // ...(C)

クラススコープにnew/delete演算子を用意した場合、そのクラスをnew/deleteする場合には常に用意したものが使われるようになります。例えば、以下のようなクラスを用意した場合、new sth()とした場合(1)が、delete psとした場合は(2)が呼ばれます。これは大切なことなのですが、両者は必ず対応させてオーバーロードするようにしてください。つまり、newの動作のみをオーバーロードしたいと思っても、deleteもオーバーロードすることが強く推奨されます。

struct sth
{
  void* operator new(std::size_t) ; // usual new...(1)
  void operator delete(void*) noexcept ; // usual new...(2)
};
...

sth* p = new sth(); // (1)が呼ばれる
delete p; // (2)が呼ばれる

ちなみに、deleteのシグニチャに(C)のものを使うと、そちらがusual deleteという扱いになります。この場合、第二引数には第一引数に示されるオブジェクトのサイズが渡されます。

struct sth
{
  void* operator new(std::size_t) ; // usual new...(1)
  void operator delete(void*, std::size_t) noexcept ; // usual new...(2)
};
...

sth* p = new sth(); // (1)が呼ばれる
delete p; // (2)が呼ばれる

両方用意すると、(B)のシグニチャが優先されます。その場合、(C)のシグニチャのdeleteはplacement deleteという扱いになります

struct sth
{
  void* operator new(std::size_t) ; // usual new...(1)
  void operator delete(void*) noexcept ; // usual delete...(2)
  void operator delete(void*, std::size_t) noexcept ; // placement delete...(3)
};
...

sth* p = new sth(); // (1)が呼ばれる
delete p; // (2)が呼ばれる

placement new/delete

placement new/deleteとは、usual new/deleteよりも引数を多く持つnew/deleteのことです。すなわち、以下のような文脈で呼ばれるnew/deleteのことを指します。第二引数にstd::nothrow_tを取るplacement new/deleteを想像してください。

sth* p = new(std::nothrow) sth(); // placement new

p->~sth(); // call dtor
sth::operator delete(p, std::nothrow); // call the deallocation function

言いたいことは分かります。delete式はplacement形式のものが用意されていないので、デストラクタとdeallocation functionをそれぞれ手動で呼ぶ必要があります。
placement new演算子はシグニチャがvoid* operator new(std::size_t, Args...)でなければなりません。Argsは任意の数の任意の型となります。
placement delete演算子はシグニチャがvoid operator delete(void*, Args...)でなければなりません。同様に、Argsは任意の数の任意の型となります。

// placement new/delete
void* operator new(std::size_t, Args...) ; // ...(D)
void* operator delete(std::size_t, Args...) noexcept ; // ...(E)

呼び出し方が変わるだけで大してusual new/deleteと変わるところはありません。usual new/delete同様に、必ず対応したplacement new/deleteを定義する必要があります。ここで、対応したnew/deleteというのは、Args...部分が同じnew/deleteという意味です。
例えば、以下のようなplacement new/deleteを考えます。

struct sth
{
  void* operator new(std::size_t n, void* p) { return p; }
  void operator delete(void*, void*) noexcept {}
};

恐らくこのsthは、何度も生成・破棄が繰り返されるなどの理由があるのでしょう。以下のように使用することが想定されます。

sth* p = static_cast<sth>(std::malloc(sizeof(sth)));
p = operator new(p) sth();

p->~sth();
sth::operator delete(p, p);
std::free(p);

operator deleteを直接呼び出すときは、引数にoperator newと同じものを入れることが推奨されます。ちなみに、コンストラクタで例外が投げられた場合、補足される前にまずoperator deleteが呼び出されますが、placement newの文脈でコンストラクタが例外を投げた場合、対応するoperator deleteが呼び出されることになります。この意味でも、usual, placementに限らずnew/deleteはセットで用意することが強く推奨されます。

operator delete(void*, std::size_t) の両義性

operator delete(void*, std::size_t)はusual deleteとして紹介しましたが、placement deleteとなりうることにも触れました。ここで、以下のような場合を考えます。

struct sth
{
  void* operator new(std::size_t, std::size_t) ; // placement new
  void operator delete(void*, std::size_t) noexcept ; // usual delete
};

このクラスsthvoid operator delete(void*)を持たないため、void operator delete(void*, std::size_t)はusual deleteとして定義されます。しかしシグニチャとしては、placement newであるvoid* operator new(std::size_t, std::size_t)と対応するものを持っています。つまり、placement newの中でコンストラクタが例外を投げた場合にも呼び出されることになります。ゆえに、このようなプログラムはill-formedであるとされ、その動作は保証されません。
void* operator new(std::size_t, std::size_t)というシグニチャを持つplacement newを定義したい場合には、void operator delete(void*)void operator delete(std::size_t)という2つのdelete演算子を書かなければならないということになります。

struct sth
{
  void* operator new(std::size_t) ; // usual new
  void* operator new(std::size_t, std::size_t) ; // placement new
  void operator delete(void*) noexcept ; // usual delete
  void operator delete(void*, std::size_t) noexcept ; // placement delete
};

グローバルスコープに定義される場合

usual new/delete

void* operator new(std::size_t) ;
void operator delete(void*) noexcept ; // C++11
void operator delete(void*, std::size_t) noexcept ; // C++14

new/delete演算子はグローバルスコープに置くこともできます。new/delete演算子は名前空間にくるむことができないので、常にグローバルスコープに定義されるnew/delete演算子は一つとなります。3static修飾子やinline修飾子を付与した場合の動作は未定義です。ユーザ定義のnew/delete演算子がない場合には標準ライブラリが用意してくれます。ユーザがこれを定義した場合でもnew/delete演算子はC++ライブラリによって暗黙的に各翻訳単位に宣言がされます4ので、全ての翻訳単位から前方宣言なしに利用することができます。
以下の例は、C++14のコードとして解釈してください。実は、グローバルスコープに定義されるnew/delete演算子のオーバーロードはC++11とC++14で挙動が変わるのです。C++11とC++14の差異についてはまた後述します。

sub.cpp
#include <cstdint>
#include <cstdlib>
#include <new>

void* operator new(std::size_t n)
{
  std::puts("usual new (sub.cpp)");
  void* p = std::malloc(n);
  return p ? p : throw std::bad_alloc();
}
void operator delete(void* p, std::size_t)
{
  std::puts("usual delete (sub.cpp)");
  std::free(p);
}
main.cpp
int main()
{
  int* p = new int(); // outputs "usual new (sub.cpp)"
  delete p; // outputs "usual delete (sub.cpp)"
}

ちなみに、クラススコープのものとグローバルスコープのものと、どちらもnew/deleteがオーバーロードされている場合、クラススコープのものが優先されます。その場合は、::newと明示すればグローバルスコープのものを使うこともできます。ただし、::newで構築したオブジェクトは::deleteで、newで構築したオブジェクトはdeleteで破棄しなくてはなりません。スマートポインタなどを使うときにこれはしばしば見つけにくいバグを生みます。

placement new/delete

void* operator new(std::size_t, Args...) ; // ...(1)
void operator delete(void*, Args...) noexcept ; // ...(2)

ただし、C++14では(2)におけるvoid operator delete(void*, std::size_t)を除きます。
また注意点はすべてusual new/deleteと変わりません。

C++11とC++14の差異

グローバルスコープにnew/delete演算子のオーバーロードを定義することについて、C++11とC++14とで大きな差異があるというお話はずっとしていましたが、ここでその差異について書いておこうと思います。

標準ライブラリが用意するnew/deleteのシグニチャの違い

C++11の時点では、グローバルスコープに定義されるoperator delete(void*, std::size_t)は全てplacement deleteでした。しかしC++14ではvoid operator delete(void*, std::size_t)と従来のoperator delete(void*, std::size_t)の両方がusual deleteとして認められるようになりました(両方定義された場合、その両方がusual deleteとして認められます。5)。これに伴い、標準ライブラリは従来のusual deleteと新しいusual deleteの両方を提供してくれるようになりました。
また、解放関数が例外によって終了した場合、その動作が未定義であるというのはC++11から変わらないのですが6、これに合わせてC++14では標準ライブラリの提供するoperator deleteにnoexcept指定がされています。ゆえに、私たちがこれを定義する場合には、C++11かC++14かを問わずnoexceptを指定すべきです。

c++11で標準ライブラリが提供する関数は以下の通りです。7

cpp11.cpp
void* operator new(std::size_t) ;
void* operator new[](std::size_t) ; 
void operator delete(void*) ; 
void operator delete[](void*) ; 

c++14で標準ライブラリが提供する関数は以下の通りです。8

cpp14.cpp
void* operator new(std::size_t) ; 
void* operator new[](std::size_t) ; 
void operator delete(void*) noexcept ; 
void operator delete[](void*) noexcept ; 
void operator delete(void*, std::size_t) noexcept ; 
void operator delete[](void*, std::size_t) noexcept ;

よって、c++14以降のグローバルスコープに定義されるnew/deleteでは、クラススコープのものとは異なり以下のような対応付けになります。

void* operator new(std::size_t) ; // usual
void* operator new(std::size_t, std::size_t) ; // placement

void operator delete(void*) ; // usual
void operator delete(void*, std::size_t) ; // usual 

推奨されるシグニチャの違い

前述の通り、c++11ではvoid operator delete(void*)しかusual deleteとして認められていませんでした。しかしc++14以降では、void operator delete(void*, std::size_t)が推奨されるみたいです。私の手元のコンパイラ(gcc5.2)では、C++14のオプション付きでvoid operator delete(void*)しか定義しなかった場合以下の警告が出てしまいました。

main.cpp:8:6: warning: the program should also define void operator delete(void*, long unsigned int) [-Wsized-deallocation]
\void operator delete(void* ptr) noexcept

また、void operator delete(void*)void operator delete(void*, std::size_t)の両方が定義された場合、後者が優先されるようです。
ideone C++11版
ideone C++14版

対応関係はどうしたと言われそうですが、規格にそうあるのだからしょうがありません。現状だと、グローバル空間にoperator new(std::size_t, std::size_t)は定義しない方がよさそうですね。

Create/Destroy objects

// usual new/delete
void* operator new[](std::size_t) ;
void operator delete[](void*) noexcept ;
void operator delete[](void*, std::size_t) noexcept ;

// placement new/delete
void* operator new[](std::size_t, Args...) ;
void operator delete[](void*, Args...) noexcept ;

この演算子の存在を忘れていた方、ご安心ください。new[]/delete[]に関しては記事の名目上作っただけであり、注意点も全てnew/deleteと同じです。new/deleteをオーバーロードする際は、new[]/delete[]もオーバーロードするようにしてください。

終わりに

当初考えていたよりも複雑な記事になってしまいました。今更ではありますが、特にグローバルなnew/deleteに関してはオーバーロードをなるべく控え、allocatorクラスを作成することを強く推奨いたします。

さて、長くなりましたが、以上でnew/delete演算子、new[]/delete[]演算子のオーバーロードについての解説を終わりたいと思います。ここまで読んでくださった方がいらっしゃったらお礼を申し上げたいと思います。
また今回この記事を書くにあたり、私自身規格を参照しながら詳細な仕様を調べました。その為、規格の解釈が間違っている可能性があることをご了承お願いいたします。同時に、誤りなど気付いた点がございましたらコメントにお願いします。
それでは、長々と失礼いたしました。改めて、読んでいただきありがとうございました。


  1. The return type shall be void*. The first parameter shall have type std::size_t (18.2). The first parameter shall not have an associated default argument (8.3.6). [n3337/n3639, 3.7.4.1] 

  2. Each deallocation function shall return void and its first parameter shall be void*. [n3337/n3639, 3.7.4.2] 

  3. A C++ program shall provide at most one definition of a replaceable allocation or deallocation function. Any such function definition replaces the default version provided in the library (17.6.4.6). [n3337/n3936, 3.7.2] 

  4. The following allocation and deallocation functions (18.6) are implicitly declared in global scope in each translation unit of a program. [n3337/n3946, 3.7.4] 

  5. The global operator delete with exactly one parameter is a usual(nonplacement) deallocation function. The global operator delete with exactly two parameters, the second of which has type std::size_t, is a usual deallocation function. 

  6. 3 If a deallocation function terminates by throwing an exception, the behavior is undefined. [n3337/n3639, 3.7.4.2] 

  7. [n3337, 3.7.4] 

  8. [n3639, 3.7.4] 

42
36
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
36