dlang
D言語Day 13

deleteがなくなる話

はじめに

以前から「そのうち消すから使わないでね」と言われていたが、いつまでたっても消えないので、「このまま残ったままになるのでは・・・」と思われていたdelete演算子ですが、dmd2.079.0(2018年3月1日リリース)でついに廃止予定となりました。一定期間の後言語から削除される予定です。

(ちなみにdmd2.080.0で、カスタムアロケータ(クラスに独自のnew/deleteを定義するやつ)もひっそりと廃止予定となりました。)

変更からだいぶ時間が経っているのですが、ネタがない気付いたのが最近だったのと一応年内の出来事なので、今さらですが書いてみることにしました。

代替の機能

以下はdeleteを使用した際に表示される警告メッセージです。

Deprecation: The `delete` keyword has been deprecated.
Use `object.destroy()`
 (and `core.memory.GC.free()` if applicable) instead.

代替機能としてobject.destroy()が案内されています。

deleteの動作は大まかには次の通りです。

  1. オブジェクトの破棄(デストラクタ)
  2. メモリの解放
  3. 参照変数の初期化(nullの代入)

このうち1.を行うのがobject.destroy()であり、2.を行うのがcore.memory.GC.free()に相当します。
元の挙動がどうしても必要な場合、core.memory.__delete()を使用することもできます。

D言語的にはメモリの解放はGCに任せてほしい、という感じでしょうか。


object.destroy()の動作(クラスの場合)は

  1. 初回のみ以下の処理を実行する
  2. メンバ変数、および本体のデストラクタを再帰的に呼び出す
  3. メンバ変数を初期状態に戻す

という感じで、object.destroy()後にオブジェクトに触っても(ある程度)安全なようになっています。

int dtorCount;
class Hoge {
    string s = "XXXX";
    this() { s = "Hoge"; }
    ~this() { ++dtorCount; }
}
void main() {
    auto p = new Hoge();
    assert(dtorCount == 0);
    assert(p.s == "Hoge");
    .destroy(p);            // 1回目は実行される
    assert(dtorCount == 1);
    assert(p.s == "XXXX");  // 初期状態に戻っている
    .destroy(p);            // 2回目以降は何もしない
    assert(dtorCount == 1);
    assert(p.s == "XXXX");
}

注意点として、object.destroy()はGCにはノータッチ(GCを実行したりメモリを解放したりしない)とドキュメントには書いてあるのですが、現時点では@nogcではないようです(object.destroy()の問題ではなく、クラスのデストラクタの問題)。

class Hoge {
    void print(string s) @nogc {
        import core.stdc.stdio;
        fprintf(stdout, "%s", s.ptr);
    }
}
void main() @nogc {
    import core.stdc.stdlib, std.conv;
    // GCを使わずに、malloc, freeでメモリ確保
    enum size = __traits(classInstanceSize, Hoge);
    void* mem = malloc(size);
    scope(exit) free(mem);
    // emplaceでインスタンスを割り当て
    auto hoge = emplace!Hoge(mem[0 .. size]);
    // 普通に使用できる
    hoge.print("hello");

    .destroy(hoge);  // Error: `@nogc` function `D main` cannot call non-@nogc function `object.destroy!(true, Hoge).destroy`
}
class Hoge { }
void main() @nogc {
    import std.experimental.allocator;
    import std.experimental.allocator.mallocator;
    auto p = Mallocator.instance.make!(Hoge)();
    // 同じ理由でこれもダメ
    Mallocator.instance.dispose(p);  // Error: `@nogc` function `D main` cannot call non-@nogc function `std.experimental.allocator.dispose!(shared(Mallocator), Hoge).dispose`
}

すぐには修正が難しい問題らしく、ちょっと残念。

廃止の理由

公式サイトのDeprecated Features(廃止予定の言語機能についてまとめたページ)のdeleteの項には

delete makes assumptions about the type of garbage collector available that limits which implementations can be used, and can be replaced by a library solution.

deleteが存在することで、D言語で使用可能なGCの種類が制限され、またライブラリ実装で置き換えが可能なため、とあります。
(※ちなみにInternetArchiveによると、2012年7月2日には既にこの記載がありました・・・。)

これだけだと、deleteは標準のGC実装に依存しているので、引き剥がすことが目的という風にも読めます。

ここがダメだよdelete

deleteの問題点として思いついたものを書いてみます。

  • オブジェクトへの参照が複数ある場合、delete後も解放済みのメモリにアクセス可能になる
import std.stdio;
class Hoge {
    string s_ = "XXXX";
    this(string s) { s_ = s; }
    ~this() { "%s.dtor()".writefln(s_); }
}
void main() {
    /// gc
    {
        scope a1 = new Hoge("a");  // オブジェクトを参照するa1とa2が消えてから
        scope a2 = a1;             // GCによってファイナライズされるので安全
    }
    "---".writeln();

    /// delete
    {
        scope b1 = new Hoge("b");
        scope b2 = b1;
        delete b1;                 // オブジェクトは削除され、b1はnullになるが
        b2.__dtor();               // b2経由で解放済みのメモリにアクセス可能であり危険
    }
}
a.dtor()
---
b.dtor()
XXXX.dtor()
  • const/immutableな変数の場合、deleteでは最後のnullの代入ができずにエラーになる(それはそう)
class Hoge { }
void main() {
    import core.memory;
    const p = new Hoge();
    delete p;  // Error: cannot modify `const` expression `p`
    .destroy(p);  // Ok
    GC.free(cast(void*)p);  // Ok
}

おわりに

今年はあまりD言語の情報を追えていなかったのですが、地味に色々と進化していて面白かったです。