最近、一部のコードがnew T[1]
を体系的に使用している(delete[]
に適切に一致している)ことを発見しました。これが無害なのか、生成されたコードに問題があるのか(空間または時間/パフォーマンス)。もちろん、これは関数やマクロのレイヤーの背後に隠されていましたが、それは要点の外です。
論理的には、どちらも似ているように見えますが、そうでしょうか。
コンパイラーはこのコードを(変数ではなくリテラル1を使用して、関数レイヤーを介して、1
がnew T[n]
を使用してコードに到達する前に2または3回引数変数に変換する]ようにすることができますか?スカラーnew T
?
これら2つの違いについて知っておくべき他の考慮事項/事柄はありますか?
いいえ、コンパイラはnew T[1]
をnew T
で置き換えることはできません。 operator new
とoperator new[]
(および対応する削除)はreplaceable([basic.stc.dynamic]/2)です。ユーザー定義の置換では、どちらが呼び出されるかを検出できるため、as-ifルールではこの置換を許可していません。
注:これらの関数が置き換えられていないことをコンパイラーが検出できた場合、その変更が行われる可能性があります。しかし、コンパイラーが提供する関数が置き換えられていることを示すソースコードには何もありません。置換は一般的にlink時に行われ、置換バージョン(ライブラリ提供バージョンを非表示にするバージョン)をリンクするだけです。 compilerでそれを知るには、一般的に手遅れです。
T
に簡単なデストラクタがない場合、通常のコンパイラの実装では、new T[1]
はnew T
に比べてオーバーヘッドがあります。配列バージョンは、要素の数を格納するために、少し大きなメモリ領域を割り当てます。そのため、delete[]
では、デストラクタをいくつ呼び出す必要があるかを認識しています。
したがって、オーバーヘッドがあります。
delete[]
は、デストラクタを呼び出すためにループが必要なため、少し遅くなります。代わりに、単純なデストラクタを呼び出します(ここでの違いはループのオーバーヘッドです)。このプログラムをチェックしてください:
#include <cstddef>
#include <iostream>
enum Tag { tag };
char buffer[128];
void *operator new(size_t size, Tag) {
std::cout<<"single: "<<size<<"\n";
return buffer;
}
void *operator new[](size_t size, Tag) {
std::cout<<"array: "<<size<<"\n";
return buffer;
}
struct A {
int value;
};
struct B {
int value;
~B() {}
};
int main() {
new(tag) A;
new(tag) A[1];
new(tag) B;
new(tag) B[1];
}
私のマシンでは、次のように表示されます:
single: 4
array: 4
single: 4
array: 12
B
には重要なデストラクタがあるため、配列バージョンの場合、コンパイラーは要素数を格納するために追加の8バイトを割り当てます(64ビットのコンパイルなので、これには8バイトが必要です)。 A
は簡単なデストラクタを実行するので、A
の配列バージョンはこの余分なスペースを必要としません。
注:Deduplicatorがコメントしているように、配列バージョンを使用するとパフォーマンスが若干向上します。デストラクタが仮想の場合:delete[]
では、コンパイラはデストラクタを仮想的に呼び出す必要がありません。 T
。これを示す簡単なケースを次に示します。
struct Foo {
virtual ~Foo() { }
};
void fn_single(Foo *f) {
delete f;
}
void fn_array(Foo *f) {
delete[] f;
}
Clangはこのケースを最適化しますが、GCCは godbolt ではありません。
fn_single
の場合、clangはnullptr
チェックを発行し、destructor+operator delete
関数を仮想的に呼び出します。 f
は、空でないデストラクタを持つ派生型を指すことができるため、この方法で行う必要があります。
fn_array
の場合、clangはnullptr
チェックを発行し、デストラクタを空にすることなく、デストラクタを呼び出さずにoperator delete
を直接呼び出します。ここで、コンパイラはf
が実際にFoo
オブジェクトの配列を指していることを知っているため、派生型にすることはできません。したがって、空のデストラクタへの呼び出しを省略できます。
ルールは単純です。delete[]
はnew[]
と一致する必要があり、delete
はnew
と一致する必要があります。他の組み合わせを使用した場合の動作は定義されていません。
as-ifルールにより、コンパイラーは実際にnew T[1]
を単純なnew T
に変換(およびdelete[]
を適切に処理)することができます。私はこれを行うコンパイラーに出会ったことはありません。
パフォーマンスについて予約がある場合は、それをプロファイルしてください。