Edited at
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言語の情報を追えていなかったのですが、地味に色々と進化していて面白かったです。