🛐

deleteとdelete[]の使い分け

に公開1

今回は私が昔から時々やってしまうミスについて話そうと思います。

C++ にはnewnew[]という演算子が存在します。

new
// int型のオブジェクトを1個ヒープでアロケートする
int* data = new int();
delete data;
new[]
// int型の要素数が5の配列ヒープでアロケートする
int* data = new int[5]();
delete[] data;

new演算子でアロケートしたメモリはdeleteで、new[]演算子でアロケートしたメモリはdelete[]で解放しないといけないです。ここまでは簡単で、恐らくみんなご存じだと思います。
でもモダンな C++ では以下のようにstd::unique_ptrのようなスマートポインタを使う方がお勧めされています。

// int型のオブジェクトを1個ヒープでアロケートする
std::unique_ptr<int> data(new int());
// int型の要素数が5の配列ヒープでアロケートする
std::unique_ptr<int[]> data(new int[5]());

std::unique_ptrは自動的にデストラクタの中でメモリを解放してくれます。newnew[]でアロケートしたメモリを区別するために、std::unique_ptr<int>std::unique_ptr<int[]>という別の書き方をしないといけないです。

でも普段new[]で配列をアロケートする時に左側に「int[] data」って書かないのに、急にこういう時はint[]を書かないといけなくて、どうしても慣れないです。だって、std::unique_ptrの山括弧の中に書くのは、そのポインタ先の型なので、頭の中でどうしてもint*intと変換されて、intだけを書いてしまいます。
でもそうすると、std::unique_ptrのデストラクタの中でdelete[]ではなくdeleteがコールされてしまいます。
でもこれを昔から何回もやってしまう理由のもう1つは、コンパイルエラーとクラッシュにならないからです。
でもメモリーリークにはなるはずですよね?

C++ のルール的にはnew[]でアロケートしたメモリをdeleteで解放すると未定義動作になります。未定義動作って呼んでいるのは、C++ の標準規格ではこういう時の動作が定義されていないからです。
cppreferenceですとこちらになると思います。

でも実際は C++ はコンパイラによって実装されているため、未定義動作って殆ど存在しないと思います。各コンパイラがそういう時にどうするかを決めないといけないです。
new[]でアロケートしたメモリをdeleteした時の動作も、コンパイラが各自で決めていると思います。コンパイラによって動作が違うかもしれないですが、未定義ではないはずです。

では、試しにdeletedelete[]の動作の違いを想像してみます。

  • newnew[]も合計で必要なメモリを計算して恐らくmalloc()で獲得するため、deletedelete[]もその獲得したメモリを恐らくfree()で解放します。これはオブジェクトを作成する前の動作なので、1つのオブジェクトでも配列でも、この点に関しては動作が変わらないですし、このメモリはどちらを使っても解放されます。
  • その後、deleteは1つのオブジェクトという前提でそのオブジェクトのデストラクタをコールします。それに対して、delete[]は配列という前提でその全要素のデストラクタをコールします。
  • intのような簡単な型(POD)の場合は、デストラクタが存在しないです。厳密にはクラスとの互換のために、trivial destructorという自明なデストラクタを持っていますが、その中では実質何もしないです。なので、簡単な型の場合は、deletedelete[]はほぼ同じ挙動になって、new[]でアロケートした簡単な型の配列をdeleteで解放しても、殆どメモリーリークにならないかなと思います。
    • new[]の場合は、コンパイラーが要素数をどこかに保存しているはずなので、そこだけdeleteの場合は解放されないと思います。
  • デストラクタが存在する複雑な型の場合は話が違ってきます。new[]でアロケートした複雑な型の配列をdeleteで解放すると、最初の要素のデストラクタのみがコールされて、他の要素のデストラクタがコールされないため、ヒープメモリ上に作成されたメンバー変数のメモリが解放されないまま、メモリーリークになるかもしれませんね。

もちろんこれはただの想像で、やはり未定義動作は避けた方がいいですね。

そもそもなんでdelete[]が存在するのかを不思議に思ったことがありますか?
newでオブジェクトを1つ作ることを要素数が1の配列を作ることと同じとすれば、newを使った時に要素数を1として保存して、delete[]の代わりにdeleteでいつも要素数によって全要素のデストラクタをコールするようにすれば、deleteだけで済みます。
そうすると、deletedelete[]を使い分ける必要がなくなり、C++ が安全かつ使いやすくなります。完璧じゃないですか。

でも C++ には「You don't pay for what you don't use.」という原則があります。deleteをどの要素数にも対応できるようにすることで、要素数が1のケースのパフォーマンスが落ちたら、意味がないです。C++ は機能を使いやすくするためにパフォーマンスを犠牲にしないです。
後、new/deleteは C言語から受け継いだmalloc/freeの代わりに使うように導入されたので、メモリ消費量とパフォーマンス的に同じでないとプログラマーはパフォーマンスを求めてmalloc/freeの方を使い続けてしまいます。
要素数が1のケースでも、要素数を保存したり、全要素のデストラクタをコールするためにforループを回したりしたら、どうしてもmalloc/freeよりメモリ消費量が増えてパフォーマンスが落ちてしまいます。
この原則のせいで、C++ の機能は他の言語と比べると使いづらかったりします。

new[]でアロケートする場合は、ちゃんとstd::unique_ptr<int[]>と書くようにしましょう。

(でも間違いやすいですけどね・・もしこう書けたら良かったかなと思ったりもします)

std::unique_ptr data(new int[5]());

|cpp記事一覧へのリンク|

Discussion

dameyodamedamedameyodamedame

unique_ptrがお勧めされているかは知りませんが、生成にはC++14以降ならstd::make_uniqueを使うのが一般的だと思います。

foo.cpp
#include <iostream>
#include <memory>

struct s{
    int value_;
    s(int value): value_{value} {std::cout << "constructed" << std::endl;}
    ~s() {std::cout << "destructed" << std::endl;}
};

auto create_s() {
    return std::make_unique<s>(1);
}

void func(std::unique_ptr<s>&& p) {
    std::cout << p->value_ << std::endl;
    std::cout << "func() finisehd" << std::endl;
}

int main() {
    func(create_s());
    std::cout << "func() was called" << std::endl;
    return 0;
}
// constructed
// 1
// func() finisehd
// destructed
// func() was called

この例ではcreate_s()でヒープ上にsのインスタンスが生成(new)され、func()の終了時に破棄(delete)されているように見えます。

よく言われるのは、この例のように生のポインタを使用しないことと、new/deleteを極力書かないことではないかと思ってました。サイズがないのでstd::vectorを使った方がいいと思いますが、配列ならこんな感じになります。

bar.cpp
#include <iostream>
#include <memory>

int main()
{
    const size_t s = 5;
    auto p = std::make_unique<char[]>(s);
    size_t i = 0;
    p[i++] = 'h';
    p[i++] = 'e';
    p[i++] = 'l';
    p[i++] = 'l';
    p[i++] = 'o';
    for (i = 0; i < s; ++i) {
        std::cout << p[i];
    }
    std::cout << std::endl;
    return 0;
}