web-dev-qa-db-ja.com

スカラー `new T`と配列` new T [1] `

最近、一部のコードがnew T[1]を体系的に使用している(delete[]に適切に一致している)ことを発見しました。これが無害なのか、生成されたコードに問題があるのか​​(空間または時間/パフォーマンス)。もちろん、これは関数やマクロのレイヤーの背後に隠されていましたが、それは要点の外です。

論理的には、どちらも似ているように見えますが、そうでしょうか。

コンパイラーはこのコードを(変数ではなくリテラル1を使用して、関数レイヤーを介して、1new T[n]を使用してコードに到達する前に2または3回引数変数に変換する]ようにすることができますか?スカラーnew T

これら2つの違いについて知っておくべき他の考慮事項/事柄はありますか?

39
ddevienne

いいえ、コンパイラはnew T[1]new Tで置き換えることはできません。 operator newoperator new[](および対応する削除)はreplaceable([basic.stc.dynamic]/2)です。ユーザー定義の置換では、どちらが呼び出されるかを検出できるため、as-ifルールではこの置換を許可していません。

注:これらの関数が置き換えられていないことをコンパイラーが検出できた場合、その変更が行われる可能性があります。しかし、コンパイラーが提供する関数が置き換えられていることを示すソースコードには何もありません。置換は一般的にlink時に行われ、置換バージョン(ライブラリ提供バージョンを非表示にするバージョン)をリンクするだけです。 compilerでそれを知るには、一般的に手遅れです。

9
Pete Becker

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オブジェクトの配列を指していることを知っているため、派生型にすることはできません。したがって、空のデストラクタへの呼び出しを省略できます。

26
geza

ルールは単純です。delete[]new[]と一致する必要があり、deletenewと一致する必要があります。他の組み合わせを使用した場合の動作は定義されていません。

as-ifルールにより、コンパイラーは実際にnew T[1]を単純なnew Tに変換(およびdelete[]を適切に処理)することができます。私はこれを行うコンパイラーに出会ったことはありません。

パフォーマンスについて予約がある場合は、それをプロファイルしてください。

5
Bathsheba