見出し画像

C++ 再入門 その14 ヒープからのメモリ割り当てと返却(4) - placement new(配置new)

さて、ヒープからのメモリ割り当てについてアレコレ書いてみましたが、最後に自分で確保したメモリにオブジェクトを割り当てる「配置new」と呼ばれる仕組みを見てみます。実際にこれを使った記憶はありませんが、この機会に使い方を調べました。

まず「new演算子」の使い方をおさらいしてみましょう。

void* operator new(std::size_t size);
void* operator new[](std::size_t size);

一般的な new演算子の構文

実は例外が絡むとこれら以外の形式もあるのですが、基本は単純な割り当てか配列を割り当てるかの二択です。これらの new 演算子を使ってオブジェクトを作ったら、最終的に対応する delete 演算子を呼び出してオブジェクトを破棄しなければなりません。

new 演算子は必要なメモリを確保した後に、そのクラスのコンストラクタを呼び出すのですが、new 演算子に引き数を渡せば、その内容を使ってコンストラクタを呼び出します。引き数が無ければ引き数のないコンストラクタが呼び出されますし、コンストラクタに引き数の省略時の値が設定されていれば、それが使われます(配列形式の場合はコンストラクタに引き数を渡すことが出来ないので常に引数なしのコンストラクタが呼び出されます)。

#include <iostream>
using namespace std;

class classA {
public:
  classA() { i = -1; };
  classA(int d) { i = d; }
  int i;
};

int main() {
  classA* a1 = new classA;
  cout << a1->i << endl;
  delete a1;

  classA* a2 = new classA(1);
  cout << a2->i << endl;
  delete a2;

  classA* a3 = new classA[3];
  for(int i = 0; i < 3; i++)
	cout << i << ":" << a3[i].i << endl;
  delete[] a3;
}

-1
1
0:-1
1:-1
2:-1

実行結果

単純な型に対するnew演算子の場合は、コンストラクタは無いので、その型に対する初期化子の構文が使えます。

#include <iostream>
using namespace std;

int main() {
  int* i = new int{ 1 };
  cout << *i << endl;
  delete i;

  char* c = new char[] { "abc" };
  cout << c << endl;
  delete[] c;
}

1
abc

実行結果

配置 new という形式は、new 演算子に任意の型のポインタを渡し、その後ろにクラスを指定します。この形式で呼び出すと処理系は「メモリを確保せず」に渡されたポインタを使ってコンストラクタを呼び出しそのメモリにオブジェクトを作ってくれます。

#include <iostream>
using namespace std;

class classA {
public:
  classA() { cout << "Construct" << endl; }
  ~classA() { cout << "Destruct" << endl; }
};

int main() {
  void *p = malloc(sizeof(classA));
  classA *o = new(p) classA;
  cout << hex << p << endl;
  cout << hex << o << endl;
  o->~classA();
  free(p);
}

Construct
0000028385E06F90
0000028385E06F90
Destruct

実行結果

オブジェクトが割り当てられるメモリは自力で確保して、それを渡します。そして解放も自分でやらなければなりません(delete演算子は使えないので明示的にデストラクタを呼びます)。

operator new

【C++】placement new

もちろん配置の配列形式も使えます。

#include <iostream>
using namespace std;

class classA {
public:
  classA() { cout << "Construct" << endl; i = 1; }
  ~classA() { cout << "Destruct" << endl; }
  int i;
};

int main() {
  void *p = malloc(sizeof(classA)*3);
  classA *o = new(p) classA[3];
  cout << hex << p << endl;
  cout << hex << o << endl;
  cout << o[0].i << endl;
  cout << hex << &(o[0].i) << endl;
  cout << hex << &(o[1].i) << endl;
  cout << hex << &(o[2].i) << endl;
  o[0].~classA();
  o[1].~classA();
  o[2].~classA();
  free(p);
}

Construct
Construct
Construct
0000021C9D844BB0
0000021C9D844BB0
1
0000021C9D844BB0
0000021C9D844BB4
0000021C9D844BB8
Destruct
Destruct
Destruct

実行結果

配列形式を使うと要素の数だけコンストラクタが呼び出され、ちゃんと配列としてアクセスできることは確認できました。配置型の場合、必ずしもデストラクタを呼ばなくても良いのからかもしれませんが、delete[] が使えないので個別に呼ぶしか無いのかもしれません。実は良い方法があるのかなぁ?ご存じの方はコメントで教えていただければ幸いです。

Placement syntax

どうしてこんな構文が導入されたのかは定かではないのですが、一般的な説明には「自分でメモリ管理を行うとき」に使うと書いてあります。確かに標準のC++のメモリ管理に則れば多くのオブジェクトを生成、破壊するとヒープ領域が断片化したり、そもそもメモリの割り当てのコストも馬鹿にならないこともあります。それを嫌う場合には配置newを使わずとも、自力で大きなブロックを割り当て、そのブロックに対する参照でオブジェクトを生成、破棄することで、大規模なヒープ操作を避けることもあります(参照ばかりで操作すればうっかり大きな領域をコピーすることもなくなります)。

どちらかというと、この機構は他の言語で作られたライブラリを呼ぶ時であったり、他のプロセスとメモリを共有する時に便利なものかもしれません。今回は触れなかったのですが、new演算子はコンストラクタを呼び出しますし、ここで例外が発生すると、確保した領域がどこまできちんと解放されるか怪しいところがあって、例外を禁止する書き方をすることも出来ます。

また細かいところですが、メモリアライメントの調整を行う方法も追加されています。構造体の時代からアライメント調整は面倒なもので、普通にコンパイルする時には問題にならないようなコードが生成されるものの、オプションによってはパフォーマンスよりもサイズが優先されて、その場合のみバグが出てしまうなんていうこともありました。いろいろな環境で動作させるコードであれば、処理系ではなく言語の方で面倒を見るというのは正しい進化なのだと思います。

align

アライメント指定されたデータの動的メモリ確保 [P0035R4]

さて充分に new は堪能できたと思うので、そろそろC++の大黒柱である class の継承に進みましょう。

ヘッダ画像は、以下のものを使わせていただきました。

https://commons.wikimedia.org/wiki/File:ISO_C%2B%2B_Logo.svg
Jeremy Kratz - https://github.com/isocpp/logos , パブリック・ドメイン,
https://commons.wikimedia.org/w/index.php?curid=62851110による

#プログラミング #プログラミング言語 #プログラミング講座 #CPP #C言語 #ヒープ #動的メモリ割り当て #配置new #Placement #new #delete #align


この記事が気に入ったらサポートをしてみませんか?