最終更新日時(UTC):
が更新

履歴 編集

動的メモリ確保の省略の許可(C++14)

このページはC++14に採用された言語機能の変更を解説しています。

のちのC++規格でさらに変更される場合があるため関連項目を参照してください。

概要

以前の仕様では、new式による動的メモリ確保はコードに書かれた通りに実行されなければならず、ひとまとめにしたり省略したりすることはできなかった。

メモリ確保の最適化のためにこの制限は緩和され、実装はnew/deleteの呼び出しをまとめたり省略したりすることができるようになる。

void lump() {
  // 個別のnew/deleteの呼び出しを
  int* p1 = new int{1};
  int* p2 = new int{2};
  int* p3 = new int{3};

  delete p1;
  delete p2;
  delete p3;

  // このようにまとめることが許可される
  int* p = new int[3]{1, 2, 3};

  delete[] p;
}

void emit() {
  // 確保サイズが分かっているようなnewの呼び出しを
  int* p = new int{10};
  delete p;

  // 通常の変数宣言のように置き換えても良い
  int n = 10;
}

ただし、このようなまとめと省略は最適化の一環として許可されているものに過ぎず、必ず行われるわけではない。

また、これらの機能と直接関係するものではないが、ユーザー定義されたものも含めたoperator new, operator deleteおよびCライブラリのmalloc, calloc, free, reallocの呼び出しはデータ競合を起こさない事が規定された。

仕様

実装は、オーバーロード可能なグローバルの割り当て関数(::operator new/::operator new[])の呼び出しを省略できる。その場合、(確保されるはずだった)ストレージは実装によって提供されるか、別のnew式によるアロケーションを拡張してあてがわれる。

ただし、new式の呼び出しe1のアロケーションを拡張して別のnewe2のストレージを提供する事ができるのは、アロケーションの拡張が行われなかった時にそれらが次の条件を全て満たしている場合に限る

  1. e1の評価はe2の評価よりも前に順序づけられる
  2. e1がストレージを確保するならば、e2が呼び出される
  3. e1e2は同じオーバーロード可能なグローバルの割り当て関数を呼び出す
  4. e1e2で呼び出される割り当て関数が例外を投げる場合、e1e2のどちらの評価で発生した例外でも、まず同じハンドラでキャッチされる
  5. e1e2によって返されるポインタ値は、評価されるdelete式のオペランドである
  6. e2の評価は、e1によって生成されたポインタ値をオペランドにとるdelete式の評価の前に順序づけられる

void ok () {
  try {
    int* e1 = new int{1};
    int* e2 = new int{2};

    delete e2;
    delete e1;
  } catch(...){}
}

void ng1 () {
  // NG、e2 -> e1の順で確保されている
  // この場合、e2を拡張してe1を省略することはできる
  int* e2 = new int{2};
  int* e1 = new int{1};

  delete e1;
  delete e2;
}

void ng2 (bool cond) {
  int* e1 = new int{1};

  // NG、e2は必ずしも評価されない
  if (cond) {
    int* e2 = new int{2};

    delete e2;
  }

  delete e1;
}

void ng3 () {
  // NG、同じ::operator newを呼び出さない
  int* e1 = new int{1};
  int* e2 = new int[]{2};

  delete e1;
  delete[] e2;
}

void ng4() {
  try {
    int* e1 = new int{1};

    // NG、最初にキャッチされるハンドラが異なる
    try {
      int* e2 = new int{2};
      delete e2;
    } catch (...) {}

    delete e1;
  } catch(...){}
}

void ng5() {
  // NG、片方または両方がdeleteされていない
  int* e1 = new int{1};
  int* e2 = new int{2};

  delete e2;
  //delete e1;
}

void ng6 () {
  int* e1 = new int{1};
  delete e1;

  // NG、e2の前にe1が解放されている
  int* e2 = new int{2};
  delete e2;
}

ここでのNGはコンパイルエラーとなるわけではなく、メモリ確保省略がなされないことを表している。

すなわち、e2の確保するメモリ領域の生存期間はe1のそれに完全に包含されており、どちらもきちんとdeleteされ、同じ関数経由でメモリを確保している場合にのみ、e1e2によるメモリ確保は統合される。

このようなnew式の呼び出しe1の割り当てが拡張された場合、拡張後に呼び出される割り当て関数のsizeパラメータ(要求サイズ)は元のe1e2で指定されていたsizeの合計値に、確保された領域にオブジェクトをアラインさせるために必要なパディングサイズを加えた値を超えない。

delete式では、そのオペランドのポインタが割り当てが拡張されたnew式(e1)から返されたものであり、e1を拡張することによってストレージを提供されていた他の全てのnew式(e2)から返されたポインタに対するdelete式が評価済である場合、そのdelete式はe1を拡張して得られた領域を解放する。

そうではない場合のdelete式、すなわちe2から返されたポインタに対するdelete式は、解放関数(::operator delete)を呼び出さない(領域上のオブジェクトの破棄のみを行う)。

したがって、省略された::operator new呼び出しに対応する::operator deleteの呼び出しもまた省略される。

この機能が必要になった背景・経緯

動的メモリ確保と解放はとても重い処理であり、パフォーマンスの最適化のためには可能な限り回避する事が望ましい。ただし、呼び出しのミクロな最適化とメモリ割り当て戦略のマクロな最適化は区別される必要があり、それを考慮してメモリ確保を最適化するにはプログラムの実行時の振る舞いや、そこから提供されるヒントを考慮した最適化が必要となる。

しかしC++11の仕様に厳密に従えば、new/delete式の呼び出しはその呼び出しから直接得られる情報しか用いてはならず、newによるメモリ確保はコードに書いた通りに実行されなければならなかった。そのため、ミクロな最適化もマクロな最適化も妨げられており、動的メモリ確保・解放処理の最適化を阻害していた。

C++11時点で既に、そのような最適化を行うメモリアロケータ(TCMalloc)やコンパイラが存在しており、それらの存在を追認しかつ動的メモリ確保・解放処理の更なる最適化を可能とするために規格書の文面を調整することとなった。

それによって、ミクロな範囲でのnew/delete式の省略が許可され、マクロな範囲の様々な情報を考慮してそれを行う事が可能となった。

関連項目

参照