はじめに
D言語の公式コンパイラである dmd が更新され、最新バージョンである 2.111.0
が 2025/04/01 にリリースされました。
今回は最終リリースの 2.110.0 から約1ヶ月でリリースされ、D言語史上でもかなり早い部類のリリースです。
しかしこれは前回リリースが遅れていたことの反動で、更新内容はかなり充実しています。(というか多すぎる!)
今回は動作確認しきれないものも多いのですが、簡単な紹介と共に一通りの内容をご紹介していきます。
より細かい内容は下記リンクからChangeLogをご覧ください。
- ChangeLog
また、前回である6月版のリリースまとめは以下になります。
- D言語の更新まとめ 2025年3月版(dmd 2.110.0)
変更点目次
言語・コンパイラ変更点
変更点
- ストレージクラス
ref
とauto ref
が、ローカル変数、静的変数、extern
変数、グローバル変数にも適用可能になりました。 - キーワード
auto
とref
は、必ず隣接して記述する必要があります。 -
align
属性で、デフォルト値を明示的に指定できるようになりました。 -
delete
キーワードが廃止されました。 - 複数の値を持つ
case
でのフォールスルーがエラーとして扱われるようになりました。 - コンストラクタで、フィールドのデストラクタがより厳しい属性を持つ場合、エラーが発生するようになりました。
- フィールドのデストラクタがより緩い属性を持つ場合、コンストラクタでエラーが発生するようになりました。
- フィールドを自身で初期化することが非推奨となりました。
- 異なる型のポインタ同士の引き算がエラーとして扱われるようになりました。
-
nothrow
関数のin/out
契約が例外をスローする場合、エラーが発生するようになりました。 - 型安全な可変長クラスパラメータが非推奨となりました。
-
debug
またはversion
ステートメントで使用される整数が言語仕様から削除されました。 - 多くのエラーメッセージが改善されました。
- コンパイラが
-extern-std=c++23
オプションをサポートするようになりました。 - DMD にビルド時間のプロファイリング機能が追加されました。
- ビルトインビットフィールド用の新しい特性
getBitfieldOffset
とgetBitfieldWidth
が追加されました。 - コンパイラフラグ
-i
が C ソースファイルを正しく認識するようになりました。 - ImportC の
pragma
により、nothrow
、@nogc
、pure
を設定できるようになりました。 - mixin テンプレートのインスタンス化で代入構文が使用可能になりました。
- オブジェクトファイル拡張子
.o
と.obj
がすべてのプラットフォームで受け入れられるようになりました。 - Objective-C セレクタが
@selector
で指定されていない場合に自動的に生成されるようになりました。 - 新しいコンパイラスイッチ
-oq
が DMD に追加されました。 - 配置new(Placement new)式が導入されました。
-
-H
および-D
用のポストフィックス型修飾子メソッド属性が追加されました。 - DMD インストールパッケージからサンプルフォルダが削除されました。
- 新しいキーワード
__rvalue
が追加されました。 - 非属性関数の安全性をチェックするための
-preview=safer
スイッチが追加されました。 - 短縮メソッド構文がコンストラクタでも使用可能になりました。
改善点
- Bugzilla 9811: 静的コード解析(PVS-Studioのような診断)を追加
- Bugzilla 9997: UFCS のスペルミスの提案を追加
- Bugzilla 10023:
ModuleInfo
にrtInfo
(または同等のもの)を追加 - Bugzilla 18235: D2 テストスイートの論理的に類似したテストを同じモジュールにグループ化
- Bugzilla 20516: D 2.0 FAQ の改善
- Bugzilla 20614: CTFE が
typeid(stuff).name
をサポートするが、classinfo.name
はサポートしない - Bugzilla 20888:
std.range.Cycle
が const と組み合わせられない - Bugzilla 20960:
-profile=gc
がnew
で割り当てられたクラスオブジェクトを追跡しない - Bugzilla 20982: 非推奨メッセージを抑制するための pragma を追加
- Bugzilla 21564: mixin テンプレートのインスタンス化に代入構文を許可
- Bugzilla 23449: スペルチェッカーがポインタメンバーの修正を提案する
- Bugzilla 23812: ImportC: インポートされた C 関数に関数属性を追加することを許可
- Bugzilla 24639: ImportC: 列挙型変換のための負の定数の定義が検出されない
- Bugzilla 24645: 20 個以上のエラーがある場合、隠れた静的アサーションエラーメッセージ
- Bugzilla 24738:
core.interpolation
のインポート提案 - Bugzilla 24745: 誤った構文で連想配列を作成した場合のエラーメッセージを改善
- Bugzilla 24749: "throw" のみからなる節は、unlikely パスであるべき
ランタイム変更点
変更点
-
core.sys.windows.bcrypt
に Windows BCrypt バインディングが追加されました。 -
core.builtins
にexpect
,likely
,unlikely
,trap
が追加されました。 -
criticalRegionLock
は削除されました。 - 新しいセグメンテーションフォルトハンドラが追加され、Linuxでnullアクセス/コールスタックオーバーフローのバックトレースが表示できるようになりました。
改善点
- Bugzilla 17416:
SocketOption.REUSEPORT
が Linux で使用できない - Bugzilla 19369:
core.sys.posix.setjmp
が Darwin をサポートしていない - Bugzilla 20567: GC は単純なプログラムでの並列マークのためにスレッドを開始しないべき
ライブラリ変更点
変更点
- 新しい関数
std.conv.bitCast
が追加されました。 -
formattedRead
の機能が拡張され、std.file.slurp
のような実行が可能になりました。 -
std.digest
に、fromHexString
とfromHexStringAsRange
関数が追加されました。 -
etc.c.odbc
の ODBC バインディングが ODBC 4.0 に更新されました。 -
std.uni
に、新しい関数popGrapheme
が追加されました。 -
std.stdio
にreadfln
とFile.readfln
が追加されました。 - スレッドセーフな
AllocatorList
としてSharedAllocatorList
が追加されました。 -
std.sumtype
に新しい手続き型 API が追加されました。 -
std.uni
が Unicode 15.1.0 から Unicode 16.0.0 にアップグレードされました。
改善点
- Bugzilla 10538:
std.typecons.wrap
がopDispatch
を考慮するべき - Bugzilla 17214:
std.array.Appender
に不必要な間接参照がある - Bugzilla 17479:
std.process.Pid
の公開コンストラクタ - Bugzilla 20330:
json
のtoString
にOutputRange
対応を追加する - Bugzilla 20889:
std.bigint.BigInt
の構築を符号とバイト配列の大きさからサポート - Bugzilla 21045:
std.getopt
: 複数値の区切りとして空白をサポート - Bugzilla 22293:
Nullable
はopCast!bool
を定義するべき - Bugzilla 24524:
RLIMIT_NOFILE
が高すぎる場合、プロセスフォークが非常に遅い - Bugzilla 24698:
Appender
はdata
プロパティを使用せずにsize_t length
の読み取り専用プロパティを公開する必要がある - Bugzilla 24823:
std.json
: JSON オブジェクトのフィールドの順序を保持することをオプションで許可 - Bugzilla 24851:
CustomFloat
の一部メンバーはconst this
を持つことができる - Bugzilla 24875:
std.traits.isAggregateType
が集約型の列挙型を集約型として考慮しない
Dub変更点
変更点
-
cImportPaths
が dmd と ldc で動作しない問題を修正しました。
dlang.org変更点
改善点
- Bugzilla 19348: 構造体キャストについてのドキュメントが不足している
- Bugzilla 24659: メモリセーフDのページに
return ref
に関する情報が不足している - Bugzilla 24868: 構造体から静的配列へのキャストがドキュメント化されていない
- Bugzilla 24876: スライスから静的配列へのキャストがドキュメント化されていない
- Bugzilla 24890: spec/arrays.dd に比較についての記載が必要で、ダングリング
.ptr
に関する警告を追加すべき
注目トピック
言語・コンパイラ変更点
ストレージクラス ref
と auto ref
が、ローカル変数、静的変数、extern
変数、グローバル変数にも適用可能になりました。
今回の目玉機能の1つです。
ローカル変数や静的変数、extern
変数、グローバル変数に ref
や auto ref
を適用できるようになりました。
これにより、従来ポインタを使っていたところでポインタを使うことなく、より安全なプログラミングが可能になります。
参照型の変数というとC++の T&
のようなものをイメージしてもらえれば良いかと思います。
D言語版では変数宣言に ref
をつけます。
では、出来ること出来ないこと、ちょっとした挙動を確認しておきます。
struct S { int a; }
S s;
ref int r = s.a;
r = 3;
assert(s.a == 3);
auto ref x = 0;
auto ref y = x;
static assert(!__traits(isRef, x)); // auto ref を値で初期化した場合は、ref ではない
static assert( __traits(isRef, y)); // auto ref を ref で初期化した場合は、ref になる
static assert(!__traits(compiles, {
ref int z; // ref 変数は宣言時に初期化が必要
assert(z == 0);
}));
// sizeof
auto ref int a = 0;
auto ref int b = a;
int* c = &a;
pragma(msg, "sizeof a: " ~ to!string(a.sizeof)); // 4
pragma(msg, "sizeof b: " ~ to!string(b.sizeof)); // 4
pragma(msg, "sizeof c: " ~ to!string(c.sizeof)); // 8
コンパイルエラーになるケースを確認しておくと、以下のようなメッセージが出ます。
コードの問題個所がわかりやすくなっているのが良いですね。
source\app.d(25,11): Error: variable `app.test1.z` - initializer is required for `ref` variable
ref int z; // ref 変数は宣言時に初期化が必要
^
source\app.d(25,11): Error: rvalue `0` cannot be assigned to `ref z`
ref int z; // ref 変数は宣言時に初期化が必要
^
今回の強化はポインタを回避した安全なプログラミングを可能にするものなので、仕方なくポインタにしていたところは積極的に置き換えを狙っていきたいですね。
align
属性で、デフォルト値を明示的に指定できるようになりました。
align
属性で、一度設定した内容を既定値に戻すことができるようになりました。
align(4)
などで明示的に指定した後、align(default)
とすることで、元のデフォルト値に戻すことができます。
使い方はコードを見た方が早いので、以下サンプルです。
struct S
{
align(4)
{
byte x;
align(default) long y;
long z;
}
}
void main()
{
pragma(msg, S.x.alignof); // 4
pragma(msg, S.y.alignof); // 8
pragma(msg, S.z.alignof); // 4
}
こういう機能は既定値なんだっけ?となりがちですが、default
を使う言語仕様は直観的でわかりやすいので覚えておきたいですね。
多くのエラーメッセージが改善されました。
されました!
正直手に負えない分量なので 公式の内容 を確認してください!
主な変更点をChatGPTに要約してもらうと以下の通りです!
-
@safe
関連のエラーメッセージが一貫性のある形式に変更-
@safe
関数の違反に関するエラーが明確に「@safe
が原因」とわかるように記述されるようになった。
-
-
関数属性(例: nothrow)の推論失敗時のメッセージが簡潔に
- 冗長だった推論失敗メッセージが、より読みやすく、分かりやすくなった。
-
ラムダ式(関数リテラル)の名前表示が改善
-
__lambda1
のような内部名ではなく、ラムダの本文を使って表示されるようになった(例:() => 42
)。
-
-
オーバーロードの曖昧さに「マッチレベル」が表示される
- どの関数にも合ってしまう場合、なぜ曖昧なのか(暗黙の変換後に複数マッチ)という情報が追加された。
-
演算子オーバーロード関連のエラーメッセージが詳細化
- 定義がない場合、どの関数を定義すればよいか提案される。
- テンプレート関数が存在してもインスタンス化に失敗した場合、その詳細なエラーが表示される。
-
旧式の演算子オーバーロード(D1スタイル)は完全に削除
-
opAdd
,opDot
などD1スタイルの演算子関数は完全にサポート終了。エラーメッセージにも登場しなくなった。
-
-
クラスの
auto new()
アロケータは構文的にも無効に- 以前は意味解析におけるエラーだったが、現在はパース段階でエラーとなる。
コンパイラが -extern-std=c++23
オプションをサポートするようになりました。
D言語の公式コンパイラであるDMDが、-extern-std=
オプションで c++23
を指定できるようになりました。このオプションを利用することで、C++23標準をターゲットとした外部コードとの連携が可能になります。
現在このオプションは、__traits(getTargetInfo, "cppStd")
の値を 202302
に設定するだけです。今後これを活用することで、D言語コード内でターゲットとなるC++標準のバージョンを取得し、それに応じた処理を実装することができると期待されています。
例えば、以下のコードでターゲットのC++標準を確認できます。
import std.stdio;
void main() {
writeln(__traits(getTargetInfo, "cppStd")); // 出力例: 202302
}
この機能は、C++との相互運用性をさらに向上させるための重要なステップであり、特に最新のC++標準を活用するプロジェクトにおいて非常に有用です。
詳細については、公式ドキュメントの __traits(getTargetInfo) を参照してください。
また、C++23の新機能についてはこちらが参考になるかと思います。
DMD にビルド時間のプロファイリング機能が追加されました。
今回の目玉機能の1つです。
DMDのビルド時間について、詳細なプロファイリング機能が実装されました。
コンパイル時のフラグとして -ftime-trace
を指定することで、どの部分がコンパイルに時間がかかっているのかを調査することができます。
また、出力ファイルを指定するには -ftime-trace-file=trace.json
を使用します。
使用例は以下のようになります。
dmd -ftime-trace -ftime-trace-file=trace.json app.d
また、DUBのsdlファイルで指定する場合は以下のような指定になります。
dflags "-ftime-trace"
dflags "-ftime-trace-file=trace.json"
結果ファイルはChromeやEdgeのパフォーマンスタブで表示できる形式になっています。
表示してみるとこんな感じになります。
dmd使ったコンパイル時間のプロファイル機能(-ftime-trace)が思ったよりあっさり動いた。結果はEdgeの開発者ツールで読める。 pic.twitter.com/bmanMUJaF2
— lempiji@思秋期 (@lempiji) April 3, 2025
ちなみにここで初めて知ったんですが、LDCには既に同じ名前のフラグがあり、同じようにビルド時間の調査が可能です。
ビルトインビットフィールド用の新しい特性 getBitfieldOffset
と getBitfieldWidth
が追加されました。
D言語において、ビットフィールドの introspection(内部構造の調査)を可能にする新しい特性 getBitfieldOffset
と getBitfieldWidth
が追加されました。この機能により、ビットフィールドのオフセット(開始位置)や幅(ビット数)を簡単に取得できるようになります。
この機能は -preview=bitfields
フラグを使用して有効にして使います。
使い方は以下のようになります。
struct S
{
int a, b; // 通常の整数フィールド
int :2, c:3; // ビットフィールド
}
static assert(__traits(getBitfieldOffset, S.b) == 0); // フィールド b のオフセットは 0 ビット
static assert(__traits(getBitfieldOffset, S.c) == 2); // フィールド c のオフセットは 2 ビット
static assert(__traits(getBitfieldWidth, S.b) == 32); // フィールド b の幅は 32 ビット
static assert(__traits(getBitfieldWidth, S.c) == 3); // フィールド c の幅は 3 ビット
ビットフィールドのオフセットや幅を手動で計算するのは煩雑なので、プログラム内で正確かつ簡単にビットフィールドの情報を取得できるようになるのは可読性や保守性の向上につながります。
また、メタプログラミングでも活用できるので、今後より効率的なライブラリが期待されますね。
コンパイラフラグ -i
が C ソースファイルを正しく認識するようになりました。
ちょっとした破壊変更になりうるので注意が必要な変更です。
今回対象となるこの -i
フラグは、主にディレクトリを指定して import
で読み込む探索先を追加するために使うものです。今回の変更により .d
や .di
だけでなく .c
も読み込むようになります。
何が危ないかというと、D言語に変換する目的で横に .c
ファイルを置いているとまとめて読み込んでエラーになるかもしれない、ということです。
もし何かコンパイルエラーが発生した場合、意図しないファイルが読み込まれている可能性があるため、.c
ファイルが読み込まれていないか確認してみてください。拡張子を変えれば良いので .c_
にするとかでも大丈夫だと思います。
ImportC の pragma
により、nothrow
、@nogc
、pure
を設定できるようになりました。
ImportC
機能において、C言語のヘッダーファイルから関数をインポートする際に、D言語の関数属性 nothrow
、@nogc
、pure
を一括で適用できる #pragma
ディレクティブが追加されました。
これにより、以下のような C コードを D に取り込む際に、安全性やGC制御の観点での属性を手動で一つ一つ付け直す手間が無くなります。
例えば、以下のようなCヘッダがあるとします:
// some_library.h
int add(int a, int b);
void log_message(const char* msg);
これを以下のように読み込みます。
#pragma attribute(push, nothrow, nogc, pure)
#include "some_library.h"
#pragma attribute(pop)
これによって、add
関数と log_message
関数は nothrow
と @nogc
属性が付与され、D言語側で nothrow
や @nogc
のコードから呼び出せるようになります。
この構文は、#pragma attribute(push, ...)
によって以降の関数定義に対して、D言語の関数属性を一時的に適用します。そして #pragma attribute(pop)
によってその設定を元に戻します。
また、認識されない属性(例えばスペルミスなど)は無視されますが、警告等は出ないため注意が必要です。
この機能は、CヘッダのD言語への移植性や安全性を高めるのに役立ち、古いCライブラリとの相互運用が多いプロジェクトでは非常に便利かと思います。
C言語と連携する強化も多いので、ImportCの機能を使う場合はぜひこちらも思い出して使ってみてください。
mixin テンプレートのインスタンス化で代入構文が使用可能になりました。
これまで alias
に見られた代入構文がテンプレートの mixin
でも使えるようになりました。
これまでは、mixin
テンプレートをインスタンス化して名前を付ける場合、以下のように名前を後ろに書く構文しか使えませんでした。
mixin MyMixinTemplate!(Args) myName; // 旧構文
myName.hello(); // 名前を付けて呼び出す
今回のアップデートにより、より直感的な「代入形式の構文」も使えるようになりました:
mixin myName = MyMixinTemplate!(Args); // 新構文
myName.hello(); // 名前を付けて呼び出す
このような名前付きのテンプレートミックスインを使うケースは多くないかもしれませんが、1つのテンプレートを違うパラメーターでいくつも特殊化する場合は呼び分けるのが便利になるので覚えておいてみてはいかがでしょうか?
alias
と同じような構文で自然に書けるので、今後はこの書き方をメインにしていくと良いかと思います。
オブジェクトファイル拡張子 .o
と .obj
がすべてのプラットフォームで受け入れられるようになりました。
DMDコンパイラが、オブジェクトファイルとしての .o
(主にUNIX系で使用)と .obj
(主にWindowsで使用)を すべてのプラットフォームで受け入れる ようになりました。
従来、DMDはプラットフォームごとに受け入れる拡張子が異なっており、Windowsでは .obj
、Linuxでは .o
しか認識しないという制約がありました。そのため、異なるプラットフォームに向けたコンパイルでこれらの中間ファイルを扱うプロセスを上手く組むことができない、という制約がある状況でした。
この変更により、オブジェクトファイルの拡張子を OS に応じて意識する必要がなくなり、クロスコンパイルの道が広がるほか、ビルドフローの簡素化・統一が実現されます。ClangやGCCといった他のモダンなC/C++コンパイラと同様の動作になるため、一般にわかりやすいビルドプロセスが組めるようになると期待されます。
クロスコンパイルというとLDCやGCCが一歩先を行っている印象がありますが、DMDもこれで並べるようになるかもしれませんね。
今後クロスコンパイルの強化や追加情報に期待したいところです。
Objective-C セレクタが @selector
で指定されていない場合に自動的に生成されるようになりました。
あまり使っている人は多くないかもしれませんが、Objective-C連携のなかなか大きい強化です。
これまでD言語の extern(Objective-C)
で定義されたメソッドに対しては、Objective-C のセレクタ(メソッド名のようなもの)を手動で @selector("名前")
と明示的に指定する必要がありました。今回のアップデートにより、@selector
を指定しない場合でも、DMD が自動的に適切なセレクタ名を生成してくれるようになりました。(すごい!)
明示的に @selector
を指定した場合は、そちらが優先されます。
未指定の場合、以下のルールによって自動的に生成されます。
自動生成ルールの詳細
-
@property
が付いた setter 関数 →setXXX:
形式のセレクタが生成される -
関数名が
isXXX
の形式になっている場合 →is
を除いた名前が setter の対象になる
例:isFluffy()
→setFluffy:
が生成される -
通常のメソッド → 引数名を使って Objective-C 形式の
foo:bar:baz:
のようなセレクタが生成される
以下が生成の例です。
extern(Objective-C)
class Fox : NSObject {
bool fluffy;
@property bool isFluffy() => fluffy;
// 自動セレクタ: isFluffy
@property void isFluffy(bool value) { fluffy = value; }
// 自動セレクタ: setFluffy:
void doSomething(int a, int b, int c) {
// 自動セレクタ: doSomething:b:c:
}
void yip(int a) @selector("bark:") {
// 明示的なセレクタ指定。これは "bark:" として扱われる
}
}
この自動生成機能により、より直感的かつ簡潔に Objective-C 対応のDコードが書けるようになります。Xcode などでよく見る Objective-C の形式とも自然にマッチし、iOS/macOS アプリとの連携やブリッジコードの記述がかなり楽になるので使ってみてください。
新しいコンパイラスイッチ -oq
が DMD に追加されました。
新しい出力名の制御オプション -oq
が追加されました。
このスイッチは、同名のモジュールが異なるパッケージに存在する場合でも、出力されるオブジェクトファイル名が衝突しないようにするためのものです。
従来、-od
オプション(出力先ディレクトリ指定)と組み合わせて複数モジュールをビルドする際、異なるパッケージに同名のファイル(例:util/app.d
、misc/app.d
)が存在すると、それぞれのビルド結果が app.obj
(または app.o
)として出力されてしまい、上書きされてしまう問題がありました。
-oq
を使うと、パッケージパス込みの名前で出力ファイルが生成されるようになり、この衝突を回避できます。
使用例
dmd -c -oq -od=. app.d util/app.d misc/app.d
このコマンドでは、以下のようなオブジェクトファイルが出力されます:
app.obj
util.app.obj
misc.app.obj
これにより、同名のモジュールを含む大規模なプロジェクトでも安全に並列ビルドや出力先の統一が可能になります。
さらにこの -oq
スイッチは、ドキュメント生成(-D
)やインターフェースファイル生成(-H
)の出力ファイル名にも適用されます。たとえば、-Dd=.
や -Hd=.
と組み合わせて使うことで、それぞれ util.app.html
や misc.app.di
のようなファイル名で出力されます。
ある種問題と言える動作が改善された形ですが、これでより安心してビルドできるようになるのは嬉しいですね。
補足:LDCとの互換性
この -oq
スイッチは、Dの別実装である LDC ではすでに存在していた機能です。今回の DMD への追加により、コンパイラ間でのビルドスクリプトやツールチェインの互換性が高まり、より移植性のある開発環境を構築できるようになります。
dubプロジェクトであれば、元々 dflags "-oq" compiler=ldc2
のように指定していたと思いますが、これからは compiler
の指定をしなくても良くなります。
配置new(Placement new)式が導入されました。
今回の目玉機能、 配置new(Placement new) です!
呼び方は色々ありますが、「配置new」や「プレイスメントnew」と呼ばれることが多いようです。
これは「メモリ確保せずに、指定した場所にインスタンスを直接構築する」ための構文です。C++を触ったことがある方には new (ptr) Type(args...)
のような構文を見たことがあるかもしれませんが、今回D言語でも同様のことができるようになりました。(つまり、同様の悪夢も付いて回るわけですが…)
基本的な使い方
class C
{
int i, j = 4;
}
void[__traits(classInstanceSize, C)] k = void; // スタック上に未初期化メモリを用意
C c = new(k) C; // kの位置にCのインスタンスを構築
assert(c.j == 4); // コンストラクタによる初期化が行われる
assert(cast(void*)c == cast(void*)k.ptr); // kの位置にインスタンスが構築されている(ポインタが同じ)
この new (...)
は左辺値なら良いので、ref
の戻り値も渡せます。malloc
を使いながら、一見すると new
を使っているように見えるのがポイントです。
import core.stdc.stdlib;
struct S { int i = 1, j = 4, k = 9; }
ref void[T.sizeof] mallocate(T)() {
return malloc(T.sizeof)[0 .. T.sizeof];
}
S* ps = new(mallocate!S()) S;
assert(ps.i == 1);
assert(ps.j == 4);
assert(ps.k == 9);
位置指定の用途では、以下のように動的配列で管理している場合が比較的わかりやすいです。
struct S { int i = 1, j = 4, k = 9; }
S[1] arr;
S* ps = new(arr[0]) S;
assert(ps.i == 1);
assert(ps.j == 4);
assert(ps.k == 9);
assert(arr[0].i == 1);
assert(arr[0].j == 4);
assert(arr[0].k == 9);
assert(ps == &arr[0]);
この配置newは GC(ガベージコレクタ)によるメモリ確保が一切発生せず、@nogc
の関数でも使用できる、というのが大きな特徴です。
ただしいくつか制限もあります。
- 明示的に危険な操作をしているため、関数には
@system
が必要です(Dではこういったローレベル操作は@system
の範疇とされます)。 - 構築先として渡す変数は スタック上や静的メモリ上の確保済み領域 でなければなりません。
- プレースメント new により構築されたオブジェクトは、デストラクタが自動で呼ばれないため、必要があれば手動で
destroy()
を使って解体する必要があります。 -
new
の左側に書く値(構築先)は、その型とサイズが一致している(またはより大きい)ことが前提です。
デストラクタを呼ぶ例は以下のようになります。
import std.stdio : writeln;
class C
{
int i, j = 4;
~this() {
writeln("Destructor called");
}
}
void[__traits(classInstanceSize, C)] k = void;
C c = new(k) C;
scope (exit) destroy(c); // デストラクタを呼ぶために destroy が必要
assert(c.j == 4);
assert(cast(void*)c == cast(void*)k.ptr);
補足
この強化された構文ですが、長くD言語を使っている方は何度か見たことがある方もいるかもしれません。
何を隠そう、new (obj) T(...);
というのは 一度廃止されたクラスアロケータと同じ構文 です。
違いは何かといえば、new
の渡す値が「スタック上の変数」であり、それに限られる(追加の引数は渡せない)ことです。
結果かなりシンプルにまとまっていますが機能的には多くの場合十分で、特にパフォーマンスに厳しいプロジェクトでは @nogc
適用拡大の武器になるはずです。
今度は消えないと思うので、少し慎重になる必要はありますが、ぜひ使ってみてください!
もし消えたら core.lifetime.emplace
などで代替できるので大丈夫です!(大丈夫か?)
-H
および -D
用のポストフィックス型修飾子メソッド属性が追加されました。
使っている方がどれほどいるか不明ですが、Dのインターフェイスファイル(.di
)や Ddoc ドキュメント生成の出力形式が変更されました。
.di
インターフェースファイルの生成(-H
スイッチ)および Ddoc ドキュメント生成(-D
スイッチ)の出力において、関数の型修飾子(例:const
や pure
)が、戻り値の型の手前ではなく、関数パラメータの後ろに記述される形式に変更されました。
従来の形式では、修飾子が戻り値の型修飾と混在していたため、関数の性質と戻り値の型の区別がわかりづらいことがありました。今回の変更により、C++ライクな構文に近づき、コードの読みやすさが向上します。
例としては以下のような感じです。
変更前(旧形式)
struct S
{
const int f(); // const が戻り値型にかかっているように見える
}
変更後(新形式)
struct S
{
int f() const; // 修飾子がメソッド本体の一部であることが明確になる
}
ちなみにこの「ポストフィックス型修飾子(postfix type qualifier)」とは、関数の型修飾子(const
, pure
, nothrow
, @safe
など)を「関数宣言の後ろ」に置く記述スタイルのことを指します。
この記法は、特に以下のようなメリットがあります:
- 修飾子が「戻り値の型修飾」と「関数本体の属性」として視覚的に明確に区別される
- 複数の修飾子が付いても可読性が高まる(例:
int foo() pure nothrow @safe const;
)
今回の変更は視覚的な改善で地味ですが、一般に「このように書いた方が良い」というコーディングスタイルの推奨でもあります。
これまでは書き順や適用規則がわかりづらい型修飾を見かけた方もいるかもしれませんが、今後はこのスタイルを一般的にしていきたいですね。
DMD インストールパッケージからサンプルフォルダが削除されました。
DMDのGitHubリポジトリからサンプルフォルダが削除されました。
インストーラーやパッケージ要領の削減が目的ですが、オンライン資料が充実しているので削除しても問題ないだろうとの判断です。
実際、基礎的な内容は公式の D Lang Tour で学び、オンラインの Playground で色々試せます。
技術記事として 令和のD言語基礎文法最速マスター 、実践応用的な内容は 日本語Cookbook がありますのでこちらを参考にしてもらえればと思います。
ちなみにコンパイラ同梱のサンプルに興味がある方は、undeaD
リポジトリに移動されたサンプルを見てみてください。
新しいキーワード __rvalue
が追加されました。
今回の重要機能の1つ、右辺値に関する新たなキーワード __rvalue
が導入されました。
このキーワードは、「本来 lvalue(左辺値)として扱われる式を、rvalue(右辺値)として明示的に扱う」ための機能です。
D言語では、同じ関数名でも引数の受け渡し方法(参照 or 値)によってオーバーロードできますが、呼び出し時の式が lvalue か rvalue かでどの関数が選ばれるかが変わります。
void foo(S s); // 値渡し:rvalue用
void foo(ref S s); // 参照渡し:lvalue用
S s;
S makeS();
foo(s); // lvalue → foo(ref S) が選ばれる
foo(makeS()); // rvalue → foo(S) が選ばれる
ここで、s
を値として渡したい場合に __rvalue(s)
を使うと、明示的に rvalue として扱うことができます。
foo(__rvalue(s)); // foo(S) が選ばれるようになる
この機能は、関数の呼び分けだけでなく、ムーブコンストラクタやムーブ代入の挙動にも関わってきます。
たとえば通常、変数を別の変数にコピーすると、メモリや内部バッファの複製が発生します。しかし、ムーブ(move)であれば中身を「渡すだけ」なので、コストが非常に軽く済みます。
ちなみにこの __rvalue
は式として使えるのですが、これ自体が何か処理をすることはありません。たとえば以下のようなコードを書いてもムーブは発生しません。
S s;
S t = __rvalue(s); // sがムーブされたり破棄されるわけではない
また公式サンプルによると、__rvalue
はなんと関数の属性としても利用でき、関数の戻り値が参照(ref
)であっても、「呼び出し元では rvalue として扱う」ことを指定できます。
ref T move(T)(return ref T source) __rvalue
{
return source;
}
S s;
S t = move(s); // move(s) の戻り値は rvalue 扱いになる
公式ライブラリでも使われているという話なのですが、探してもちょっと見つかりませんでした。
便利に使っているケースがあればぜひ教えて下さい。
補足:C++の右辺値参照(&&)との違い
この __rvalue
は、C++ の std::move
に近い性質を持ちますが、D言語では 右辺値参照 という概念は存在しません(型として T&&
は表せない)。
その代わり、__rvalue
を使って「rvalueとして扱ってほしい」という意思を明示できるようになったわけです。
右辺値の扱いとしてC++とDでかなり異なる立場を取ることになった(シグネチャとして受け取る側が右辺値を表明するのか、渡す側が選択を表明するのか)と思うのですが、ひとまずこれがD言語のやり方ということで使っていきましょう。
個人的には core.lifetime.move
や core.lifetime.forward
、std.algorithm.move
などがたくさんある中で __rvalue
が出てきたのでちょっと混乱しそうだなと思いましたが、更に最適化のツールが手に入ったということで性能が重要なシーンではどんどん活用していきたいですね。
非属性関数の安全性をチェックするための -preview=safer
スイッチが追加されました。
今回の目玉機能の1つ、D言語の安全性強化に関するプレビューフラグが追加されました。
最初に誤解が無いよう書いておくと、これは @safe
を既定値とするものでは ありません。
D言語では、関数に @safe
、@trusted
、@system
といった安全性に関する属性を指定することで、どのようなコードがその関数内で許容されるかを制御できます。
しかし、属性が何も付いていない関数(非属性関数)は、暗黙的に @system
として扱われるため、ポインタ操作などの危険なコードを書いても警告もエラーも出ず、知らぬ間に安全性が損なわれるケースがありました。
今回導入された -preview=safer
スイッチは、非属性関数でも一部の危険なコードを検出・エラーとして扱うためのプレビュー機能です。
つまり、@system
の意味が少し変わるスイッチです。
具体的に変わる点
@safe
コードでは禁止されるような操作(例:ポインタの加算など)が、非属性関数でも -preview=safer
を付けているとエラーになります。
ただし、次のような「すぐに直せないコード」(例:非属性関数や @system
関数の呼び出し)は今まで通り許容されます。
void f(); // 非属性(無指定)
@system void g(); // 明示的に @system
void main()
{
int* p;
p++; // ← エラーになる(ポインタ演算)
f(); // ← OK(非属性関数の呼び出し)
g(); // ← OK(@system関数の呼び出し)
}
これは「直せる場所はちゃんと直してね、でも既存コードへの影響は最小限に」という方針です。
この -preview=safer
は将来的にデフォルトになる可能性がありますが、互換性の問題を避けるために、今はオプトイン(明示的に有効化)という形になっています。
「D3を出すなら @safe
を既定値にしよう」という話も真面目に議論されていますが、今回はあくまでも中間ステップとしての措置、オプトインのスイッチでの対応、となっています。
というわけで以下使い方です。
dmd -preview=safer main.d
もしくは、DUB プロジェクトでは以下のように設定します。
dflags "-preview=safer"
補足ですが、詳細な設計思想や実装ポリシーについては、以下の設計文書も参考になります。
この機能は今後のD言語の「より安全なデフォルト」への第一歩です。既存コードとの互換性を保ちつつ、少しずつ @safe
な世界に近づいていく事を目指しています。
既定値が @safe
になるのはいつになるか分かりませんが、ちょっと試してみて、日頃どれくらい危険なコードを書いているのか確認してみると良いかもしれません。改善できるところは改善していきたいですね。
短縮メソッド構文がコンストラクタでも使用可能になりました。
D言語で元々使える「短縮メソッド」構文が、コンストラクタでも使えるようになりました。
この短縮メソッド構文ですが、2022/11/16のDMD 2.101.0で使えるようになっている機能です。
これまでD言語では、短縮メソッド構文(ショートハンド構文) を通常の関数には使えても、コンストラクタには使えませんでした。具体的には、関数本体を「=>
」で表す省略記法をコンストラクタで書くと、「コンストラクタから値を返すことはできません」というエラーが発生していました。
今回のアップデートにより、戻り値が void
と推定される場合に限り、短縮構文が正式にサポートされました。
例:これまではエラーだった書き方
struct Number
{
int x;
void vf(int) { x = x + 10; }
this(int x) => vf(x); // 以前はエラー!
}
このようなコードは以前は「コンストラクタから式を返すことはできません」というエラーになりましたが、今は有効な構文として扱われます。
コンストラクタでも使えるようになった条件
-
=>
の右側がvoid
を返す式であること(this(...) => expression;
) - その
expression
がthis(...)
やsuper(...)
の呼び出し、または副作用目的の関数呼び出し(返り値を使わない)であること
実用的なパターン
特に オーバーロードされたコンストラクタで別のコンストラクタを呼び出すときに便利です。
struct Number
{
int x;
this(int x)
{
this.x = x;
}
this(float f) => this(cast(int) f); // 今はこれでOK!
}
このように、**「floatからintへキャストしてから、intのコンストラクタに処理を任せる」**という初期化の共通化が、短く書けるようになります。
このように、ちょっとした文法強化ですが、日々のコード記述が簡潔になる改善です。
C#でも同じような構文が採用されていますので、同じような感覚で書けるというのはハードルが低くて良いですね。こちらも便利に使っていきましょう。
ランタイム変更点
core.sys.windows.bcrypt
に Windows BCrypt バインディングが追加されました。
Windows 固有の暗号APIである BCrypt (Windows Cryptography API: Next Generation, CNG) に対する D言語のバインディングが core.sys.windows.bcrypt
に追加 されました。合わせて core.sys.windows.sdkddkver
と core.sys.windows.w32api
も更新されています。
この変更により、D言語から Windows の暗号化機能(ハッシュ、鍵生成、暗号化・復号化など)に対して、C言語と同じようにネイティブAPIを直接呼び出して利用できるようになります。
つまり、誰かが書いたヘッダー相当のライブラリを入手したり、自分で extern(C)
でAPIを1つずつ宣言することなく、すぐに高度な暗号化機能をD言語から使えるようになるということです。便利ですね!
追加された内容と使い方
このバインディングには、たとえば以下のような関数や構造体が含まれています。
BCryptOpenAlgorithmProvider
BCryptHashData
BCryptFinishHash
BCryptGenerateSymmetricKey
-
BCRYPT_ALG_HANDLE
などの型定義
D言語からBCryptでSHA-256のハッシュを生成するコードの例は以下のようになります。
import core.sys.windows.windows;
import core.sys.windows.bcrypt;
import core.sys.windows.ntdef;
import std.exception : enforce;
import std.stdio : writeln;
import std.utf : toUTF16z;
BCRYPT_ALG_HANDLE hAlg;
NTSTATUS status = BCryptOpenAlgorithmProvider(
&hAlg,
BCRYPT_SHA256_ALGORITHM.toUTF16z(), // ここだけちょっと面倒
null,
0
);
enforce(status == 0, "BCryptOpenAlgorithmProvider failed");
// ...ハッシュデータの準備・処理を行う...
writeln("SHA256アルゴリズムプロバイダーを開きました");
BCryptCloseAlgorithmProvider(hAlg, 0);
これまでは D 言語でこれらの処理を行うには、core.sys.windows.windows
を使いながら自前バインディングを書くか、他言語から DLL を叩く必要がありました。今回の対応により Windows 環境における暗号処理がD言語だけで完結できる (範囲が増える)ことになります。
セキュリティ実装でC言語を使う必要がなくなるのは大きな進歩ですね。セキュリティ分野の拡充も進んでいるということで、今後の強化も期待したいところです。
core.builtins
に expect
, likely
, unlikely
, trap
が追加されました。
D言語の標準ランタイム core.builtins
に、新たなビルトイン関数群 expect
、likely
、unlikely
、trap
が追加されました。
これらはいずれも コンパイラが生成するコードに対して「ヒント(指示)」を与えるための低レベル機能で、パフォーマンス最適化やエラーハンドリングの補助として利用されます。LDCやGDCといったLLVM/GCCベースのコンパイラではこれらのヒントを活かして最適化が行われますが、DMD では現時点でこれらのヒントは無視されます(将来的な対応があるかもしれません)。
あくまでヒントであり、これを使うからといって必ずしも最適化が行われるわけではありませんが、CPUの分岐予測やパイプライン最適化に寄与するため、特にパフォーマンスが重要な場面では考慮する価値があります。
では使ってみた例ということで、以下それぞれの概略です。
expect(expr, expected)
:分岐予測ヒント
この関数は、expr
の値が expected
と一致する可能性が高いことをコンパイラに伝えます。コンパイラはこの情報を元に分岐の並びやコード配置を最適化し、CPUの分岐予測ミスによる性能低下を減らすことができます。
if (expect(flag, true)) {
// "flag は true であることが多い" というヒントを与える
doSomething();
} else {
handleFallback();
}
この構文はGCCやLLVMにおける __builtin_expect
に相当します。
likely(expr)
/ unlikely(expr)
:条件分岐の最適化ヒント
likely
/ unlikely
は、それぞれ「この条件は頻繁に真になる」「この条件はめったに真にならない」というヒントを与える関数です。
内部的には expect(expr, true)
および expect(expr, false)
に変換されますが、より明示的に「この条件はよく発生する/しない」と表明することができます。
if (likely(x > 0)) {
// x > 0 であることが多い
processFastPath();
}
if (unlikely(errorOccurred)) {
// エラーは稀にしか起きない
logAndExit();
}
これもまたCPUの分岐予測とパイプライン最適化に関わる重要なヒントになります。
trap()
:即座に異常終了するための命令
trap
は「ここには絶対に到達しないはず」という場所で使われる特殊な関数で、CPUのtrap
命令(またはabort関数)にコンパイルされます。
DMDの場合でもこれは動作する式として解釈され、assert(0)
相当だと思っておけばOKです。
assert(condition, "unreachable");
if (!condition) trap(); // 明示的に停止
この関数は次のようなケースで利用されます:
-
unreachable
なコードに意図的に到達したときに、即座にクラッシュさせる - セキュリティ上の理由で「絶対に実行されてはならない」コードパスを明示する
- コンパイラに対して「ここから先のコードは最適化してよい」と伝える
CPUアーキテクチャによってはハードウェア例外が発生しますし、そうでない場合は abort()
が呼ばれます。
普段のD言語の高レベル構文とは少し違い、システムプログラミング寄りの使い方になりますが、ライブラリ設計や高速化が求められる箇所では大きな効果を発揮します。
「この分岐はほとんど通らないよ」
「ここに来るのはバグだよ」
「この変数はこうなることが多いよ」
といった背景情報をコンパイラに伝えるための仕組みです。
興味がある方は LDC/GDC 環境で実際に使ってみて、最適化結果を比較してみると面白いかと思います。
GDC/LDCと共にDMDでもサポートされるようになったので、今後はより高速でポータブルなコードが書けますね。
新しいセグメンテーションフォルトハンドラが追加され、Linuxでnullアクセス/コールスタックオーバーフローのバックトレースが表示できるようになりました。
従来、D言語プログラムで null
ポインタを参照したり、無限再帰でスタックオーバーフローが起きたりすると、次のようなメッセージだけが表示され、原因の特定が非常に難しい状況がありました。
[1] 37856 segmentation fault (core dumped) ./app
これは SIGSEGV
(セグメンテーション違反)という低レベルのエラーで、CやC++でもよく見られるものです。ですが、このままだと どこで何が起きたか が全くわかりません。
従来D言語には etc.linux.memoryerror.registerMemoryErrorHandler()
という関数があり、これを使うと SIGSEGV
をキャッチして、InvalidPointerError
という例外を投げてくれます。
この仕組みにより、null
アクセス時にどの変数が問題だったのかを表示することができます。
ですが、この機能はコールスタックオーバーフロー(無限再帰など)には非対応でした。
既存のハンドラは呼び出しスタックを使って処理していたため、スタックそのものが壊れている場合には動作できないためです。
さらに、アセンブリに依存していてx86アーキテクチャ専用という制限もありました。
そこで今回これらの問題を解決するために registerMemoryAssertHandler()
が追加されました!
使い方は簡単で、以下のように version(linux)
ブロック内で registerMemoryAssertHandler()
を呼び出すだけです。
void main()
{
version (linux)
{
import etc.linux.memoryerror;
registerMemoryAssertHandler(); // 新しいハンドラを有効化
}
int* p = null;
*p = 42; // null ポインタアクセス → バックトレース付きでクラッシュ
}
あるいは、無限再帰でスタックを溢れさせてみるとこうなります(公式サンプル)
void recurse() { recurse(); }
void main()
{
version (linux)
{
import etc.linux.memoryerror;
registerMemoryAssertHandler();
}
recurse(); // スタックオーバーフロー → バックトレース表示
}
実行結果(例: dmd -g -run app.d
):
core.exception.AssertError@src/etc/linux/memoryerror.d(82): segmentation fault: call stack overflow
––––––––––
src/core/exception.d:587 onAssertErrorMsg [0x58e270d2802d]
src/core/exception.d:803 _d_assert_msg [0x58e270d1fb64]
src/etc/linux/memoryerror.d:82 _d_handleSignalAssert [0x58e270d1f48d]
??:? [0x7004139e876f]
./app.d:16 void scratch.recurse() [0x58e270d1d757]
./app.d:18 void scratch.recurse() [0x58e270d1d75c]
./app.d:18 void scratch.recurse() [0x58e270d1d75c]
./app.d:18 void scratch.recurse() [0x58e270d1d75c]
./app.d:18 void scratch.recurse() [0x58e270d1d75c]
...
...
...
注意点
- Linux専用です(現時点ではWindowsやmacOSは未対応)
- DMD の
-g
フラグを使って デバッグ情報を付けることで、関数名付きのバックトレースが得られます -
registerMemoryAssertHandler
は エラーハンドリングではなく診断用です。安全に例外処理をしたい場合は別の設計が必要です
新機能ですが、とりあえず入れておくだけでも開発効率が改善するかと思います。
GDBでデバッグするのも便利ですが、Linux環境で開発されている方は、ぜひこの新ハンドラをプロジェクトの初期化処理などに導入してみてください。
ライブラリ変更点
新しい関数 std.conv.bitCast
が追加されました。
D言語の標準ライブラリ std.conv
に、新たな関数 bitCast
が追加されました。
この関数は、ある型の値を、そのビット表現をそのまま別の型として扱うためのものです。C++でいうところの std::bit_cast
、Rustでの transmute
に相当します。
import std.conv : bitCast;
uint n = 0xDEADBEEF;
// これまではこう書いていた
writeln("Bytes of n are: ", *cast(const ubyte[4]*) &n);
// これからはより明示的にこう書ける
writeln("Bytes of n are: ", n.bitCast!(const ubyte[4]));
その他、おまけ的な使い方ですが、頻繁に使う型への変換は alias
で名前を付けておくと便利そうです。
alias asBytes = bitCast!(const(ubyte[4]));
auto bytes = n.asBytes; // より簡潔に
注意点と制限
bitCast
は std.conv.to
や std.conv.parse
と違って「ビット表現をそのまま使う」ためのものです。
そのため、変換先と元の型が同サイズでなければコンパイルエラーになります。
例えば以下のようなコードはエラーになります:
int a = 42;
auto b = a.bitCast!short; // NG: サイズが一致しないためコンパイルエラー
また、構造体などを bitCast
で変換する場合は、型のアラインメントやレイアウトが一致していることを前提とするため、注意が必要です。
Cから移植するとポインタ経由のコードを書きがちですが、今後は安全かつ表現力の高いコードを書くための新たな武器として積極的に使っていきたいですね。
formattedRead
の機能が拡張され、std.file.slurp
のような実行が可能になりました。
std.file.slurp
のような、と言われてもよくわかりませんね。解説していきます。
std.format.formattedRead
に対する強化により、D言語で 入力文字列を指定フォーマットに従って一括でパースし、型付きタプルとして受け取る 処理がより簡潔かつ安全に行えるようになりました。これにより、既存の std.file.slurp
が担っていた「ファイルや文字列から複数値をまとめて読み込む」処理に近いスタイルを、より柔軟に文字列全般で活用できるようになります。
これまで、複数の型のデータを1行の文字列や入力ストリームから読み込むには、以下のように formattedRead
と変数束縛を使って都度読み取る必要がありました。
string input = "42,3.14,hello";
int a;
double b;
string c;
input.formattedRead!"%s,%s,%s"(a, b, c);
この方法は柔軟ですが、読み取り変数が多いとコードが煩雑になりがちでした。
また、ファイル読み込み向けに用意された std.file.slurp
は slurp!(int, double)(...)
のようなタプル読み込みに対応していましたが、これは「ファイル単位」かつ「行単位」での処理に限定されており、任意の入力ソースや細かいフォーマット指定との併用には向いていませんでした。
今回のアップデートでは、formattedRead
にテンプレート引数として読み込みたい「型のリスト(複数)」を指定し、それらをパースして Tuple
として一括で返す呼び出し方が追加されました。
import std.format;
import std.typecons;
auto result = "hello!3.14:42".formattedRead!(string, double, int)("%s!%s:%s"); // 型とフォーマットを指定し、Tupleが返る
assert(result == tuple("hello", 3.14, 42));
これにより、変数宣言なしでそのままパース結果をタプルとして受け取れるため、記述が減ってシンプルになります。(分割代入構文がほしい…)
また、読み取り失敗時(入力が不足している、フォーマットと合致しないなど)は、例外(FormatException
)がスローされます。これは std.file.slurp
のように「部分的な読み取り」を許す仕様とは異なり、パース成功の厳格性が求められるケースに向いています。
使用例
auto result = "hello!3.14:42".formattedRead!(string, double, int)("%s!%s:%s");
assert(result == tuple("hello", 3.14, 42));
// フォーマットと合致しない場合は例外
assertThrown!FormatException("hello!3.14:".formattedRead!(string, double, int)("%s!%s:%s"));
また、フォーマット文字列自体もテンプレート引数として渡すことができ、コンパイル時にチェックが可能になっています。通常はこちらの方が便利そうですね。
auto result = "data:123,3.0".formattedRead!("data:%s,%s", int, double);
複数型のデータ読み込みを伴う処理を一行でスマートに書きたい場面に非常に便利です。文字列処理のライブラリ構築や簡易的なパーサ設計にも活用できるので、ぜひ試してみてください。
std.digest
に、fromHexString
と fromHexStringAsRange
関数が追加されました。
便利関数の追加です。
D言語の標準ライブラリ std.digest
に、16進文字列をバイト列に変換するための新しい関数として fromHexString
および fromHexStringAsRange
が追加されました。
これらの関数は、"deadbeef"
のような16進表現の文字列から ubyte
の配列やレンジを生成するためのものです。特に「プログラム実行時(ランタイム)」に使うことを念頭に置いて用意されています。
これまでも std.conv.hexString
テンプレートを使えば16進数→バイト列の変換は可能でしたが、これはコンパイル時の文字列リテラル専用であり、ランタイムの文字列(変数)には使えません。
また、std.conv.parse
や std.conv.to
を使っても 単一の整数型への変換しかできず、複数バイトへの変換には適していませんでした。
さらに、これらは変換のために 新たなバッファを確保するため、メモリ効率の観点でも不十分でした。
fromHexString
の使い方
fromHexString
は、入力が妥当な16進文字列であれば、それに対応するバイト列(ubyte[]
)を返す関数です。0x
プレフィックスはあってもなくてもOKで、大文字・小文字混在も問題ありません。
import std.digest : fromHexString;
import std.stdio : writeln;
writeln("0xff".fromHexString); // [255]
writeln("d41d8cd98f00b204e9800998ecf8427e".fromHexString);
// [0xD4, 0x1D, 0x8C, 0xD9, 0x8F, 0x00, 0xB2, 0x04, ...]
短い入力や空文字列、0x
だけの文字列にも対応しています:
writeln("0x".fromHexString); // []
writeln("0x1".fromHexString); // [0x01]
writeln("EBBBBF".fromHexString); // [0xEB, 0xBB, 0xBF]
fromHexStringAsRange
の使い方
一方で、fromHexStringAsRange
は、変換結果を遅延生成する「範囲(range)」を返します。これは、input
をすぐに全部変換せず、必要になったときに1バイトずつ処理することで、よりメモリフットプリントの小さい実装を可能とします。
import std.digest : fromHexStringAsRange;
import std.algorithm : map;
import std.range : array;
auto hex = "0xdeadbeef".fromHexStringAsRange;
writeln(hex.array); // [0xDE, 0xAD, 0xBE, 0xEF]
-
fromHexStringAsRange
はForwardRange
として実装されており、std.algorithm
などと組み合わせた柔軟な操作が可能です。 - 戻り値の要素型は常に
ubyte
です。
入力の妥当性チェック
また、文字列が有効な16進表現かどうかをチェックする関数として isHexString
もセットで追加されています。
実行時の検証は重要なので、こういう関数も忘れず使っていきたいですね。
import std.digest : isHexString;
assert(isHexString("0xdeadbeef"));
assert(!isHexString("hello world"));
使い方まとめ
import std.digest : fromHexString, fromHexStringAsRange, isHexString;
import std.stdio : writeln;
import std.range : array;
void main() {
auto hash = "d41d8cd98f00b204e9800998ecf8427e".fromHexString;
writeln(hash); // [0xD4, 0x1D, 0x8C, ...]
auto lazyHash = "abcdef".fromHexStringAsRange;
writeln(lazyHash.array); // [0xAB, 0xCD, 0xEF]
assert(isHexString("0x123abc"));
}
なんで std.conv
じゃなくて std.digest
に追加されたのかはちょっと疑問ですが、これらの関数はまとめて覚えておきたいですね。
今後、D言語でバイナリデータを扱う場面では、fromHexString
系関数が頻出になるかもしれません。
etc.c.odbc
の ODBC バインディングが ODBC 4.0 に更新されました。
D言語に標準で付属するC言語バインディング群 etc.c.*
の中で、ODBC(Open Database Connectivity)関連のヘッダーが含まれる etc.c.odbc
が、ODBC 4.0 に対応するよう更新されました。
この更新により、ODBC 4.0 で導入された複数の新機能が、D言語から直接利用可能になります。
ODBCは、アプリケーションとリレーショナルデータベースの間の共通インターフェースとして広く使われており、SQL Server や Oracle、MySQL など様々なDBとの接続に利用されます。
D言語でもDB接続のためのライブラリがいくつか存在しますが、ODBC対応の範囲であれば標準ライブラリのみで作れるということで、覚えておきたいポイントかと思います(言語毎にライブラリを使う例の方が多い気もしますが)
ODBC 4.0 の仕様は以下を確認してください。
この変更は「Cのヘッダ更新およびD言語バインディングの追従」であるため破壊的変更はありません。
ただ、 etc.c.odbc
を直接使っている場合は一度コンパイルして確認しておくほうが安心です。
特にクラウド連携やWeb APIとの統合を行っているプロジェクトにとっては、OAuth認証やセミ構造データのサポートが進むのは嬉しいポイントでしょう。
今後、ODBC経由での高度なクエリ処理や、複雑なデータ型を扱う場面でも、D言語を使ったより高度な実装が可能になりそうですね。
std.uni
に、新しい関数 popGrapheme
が追加されました。
個人的に嬉しい強化の1つです。
新たに追加された popGrapheme
関数は、既存の graphemeStride
と decodeGrapheme
を組み合わせたような関数で、レンジの末尾からUnicodeの「書記素(grapheme)」を1つ取り出す(popする)操作を行うものです。
書記素は、Unicode上でいわゆる「1文字」として扱われる単位ですが、実際には複数のコードポイントから構成されることがあります。
たとえば、é
= e
+ アクセント記号、または国旗絵文字 🇯🇵 = U+1F1EF
+ U+1F1F5
といったケースです。
これらを視覚的に1つの「文字」として扱うための単位が grapheme です。
D言語ではこれを正しく処理するために std.uni
モジュールがあり、今回の popGrapheme
によってさらに書記素処理が強化されました。
基本的な使い方
import std.uni;
string s = "\U0001F1EC\U0001F1E7\U0001F1EC\U0001F1E7"; // 🇬🇧🇬🇧
wstring ws = "\U0001F1EC\U0001F1E7\U0001F1EC\U0001F1E7";
dstring ds = "\U0001F1EC\U0001F1E7\U0001F1EC\U0001F1E7";
// 末尾の 🇬🇧 を削除する(削除された書記素の長さを返す)
assert(s.popGrapheme() == 8); // UTF-8 で 8バイト
assert(ws.popGrapheme() == 4); // UTF-16 で 4ワード
assert(ds.popGrapheme() == 2); // UTF-32 で 2コードポイント
// 1つ削除された状態になる
assert(s == "\U0001F1EC\U0001F1E7");
assert(ws == "\U0001F1EC\U0001F1E7");
assert(ds == "\U0001F1EC\U0001F1E7");
今後、文字列を1文字ずつ「削る」「折り返す」「切り出す」ような処理では、この popGrapheme
がより安全で自然な選択肢になっていくでしょう。ユニコード対応をしっかり行いたい方にとっては見逃せない強化ですね。
std.stdio
に readfln
と File.readfln
が追加されました。
D言語標準ライブラリ std.stdio
に、新しい入力関数 readfln
および File.readfln
が追加されました。
これは従来の readf
関数の使い勝手を改善したもので、1行の入力を安全に、フォーマット指定で読み込むための関数です。
従来の readf
は、C言語の scanf
のように動作します。非常に強力ではありますが、改行コード(\n
)をフォーマット文字列に含め忘れると、複数行にまたがって読み取ってしまうという罠があり、特に初心者にとって混乱のもととなっていました。
たとえば、次のようなコードを考えてみます。
int a;
readf("%s", &a); // 改行指定を忘れている
readf("%s\n", &a); // これが正しい
これに対して readfln
は、1行分の入力をまるごと読み込んだ上で、その中身を指定されたフォーマットでパースします。これにより、改行の有無によって挙動が変わることがなくなり、読み取りが直感的かつ安全になります。
基本的な使い方
import std.stdio;
void main()
{
writeln("数値と文字列を入力してください(例: 42 hello):");
int i;
string s;
readfln("%s %s", &i, &s); // 改行を気にせず1行で読み取り
writeln("読み取った値: ", i, ", ", s);
}
このコードでは、readfln
が1行を読み取った上で、フォーマットに従って int
と string
を順に読み込んでくれます。
内部的には、readfln
は readln
で1行読み取り → formattedRead
でフォーマット解析、という処理を組み合わせたような設計です。
これにより、読み取りの失敗・パースの失敗が明確に分離され、バグを追いやすく、安全性が向上していることがポイントです。
今後、標準入力処理を書く際は readf
よりもこちらを優先的に使うことで、より直感的で安全なコードが書けるようになると思われます。
競技プログラミングなどで入力の読み取りは多いと思いますが、こちら無駄なミスを減らすためにも覚えておくと良いかもしれません。(コンパイラの更新を待つ必要がありますが…)
スレッドセーフな AllocatorList
として SharedAllocatorList
が追加されました。
D言語のメモリアロケーションライブラリ(std.experimental.allocator
)に、新たなビルディングブロック SharedAllocatorList
が追加されました。
これは、既存の AllocatorList
に対して スレッドセーフな実装を提供するもので、複数スレッドから同時に使用できるアロケータが必要な場面で利用できます。
基本構造と使い方
SharedAllocatorList
は std.experimental.allocator.building_blocks.allocator_list
モジュールに定義されており、以下のように使います。
import std.experimental.allocator.building_blocks.allocator_list : SharedAllocatorList;
import std.experimental.allocator.building_blocks.ascending_page_allocator : SharedAscendingPageAllocator;
import std.experimental.allocator.building_blocks.null_allocator : NullAllocator;
enum pageSize = 4096;
enum numPages = 16;
alias MyAlloc = SharedAllocatorList!(
(n) => SharedAscendingPageAllocator(max(n, numPages * pageSize)),
NullAllocator
);
MyAlloc allocator;
auto b = allocator.allocate(100);
assert(b.length == 100);
assert(allocator.deallocate(b));
このコードでは、共有アロケータ(SharedAscendingPageAllocator
)を用いて AllocatorList
を構築し、さらにそれを SharedAllocatorList
でラップしています。
この構造により、アロケーターを複数スレッドから安全に使うことが可能になります。
注意点
- 通常の
AllocatorList
は スレッドセーフではありません。並列環境で使うと不具合が起きる可能性があります。 -
SharedAllocatorList
は内部で適切に同期処理(ロックなど)を行い、並行アクセス時でも安全に動作するように設計されています。 - その代わり、単一スレッドでの性能は若干落ちる可能性があります。用途に応じて選択しましょう。
Ouroboros モードとは?
指定する BookkeepingAllocator(メタデータ格納用アロケータ)として NullAllocator
を与えると、「Ouroboros モード」と呼ばれる特別な挙動になります。
これは、アロケータ自身のメタデータ(管理情報)を、アロケータ自身が確保したメモリ領域に格納するという自己完結型のモードです。外部アロケータが不要になり、全体が1つの自己管理構造として動作します。
D言語の std.experimental.allocator
は非常に柔軟で高性能なアロケータ機構を持っています。今回の SharedAllocatorList
追加により、並列環境でも安全に使える構成がより簡単に実現できるようになりました。今後 @nogc
+ マルチスレッドな場面でも、より多くのコードで AllocatorList
系アロケータを活用できそうです。性能チューニングや独自メモリ管理が必要なケースで、ぜひ試してみてください。
std.sumtype
に新しい手続き型 API が追加されました。
今回のちょっとした目玉強化かと思います。
std.sumtype.SumType
に、手続き型(procedural)スタイルで使える3つの補助関数が新たに追加されました。
-
has!T
— SumType がT
型の値を保持しているかどうかを確認 -
get!T
— 保持している値がT
型であると仮定して取り出す(型が違うとassert
) -
tryGet!T
—T
型の値を取り出す(違ったらException
を投げる)
これらの関数により、従来の match
を使った 関数型スタイルに比べて、より直感的かつ簡潔なコードを書けるようになります。
std.sumtype.SumType
は、**「複数の型のうちいずれか一つを表す」型安全な代用共用体(variant)」**です。
SumType
は以下のように型リストを指定して定義できます。
alias Value = SumType!(int, string, double);
例:従来の書き方(match
によるパターンマッチ)
import std.sumtype;
import std.stdio;
alias Value = SumType!(int, string); // int と string のいずれかを保持する型
Value v = 123;
v.match!(
(int i) => writeln("int: ", i),
(string s) => writeln("string: ", s)
);
このように match
は強力ですが、全ての型に対する分岐を用意しなければならず、簡単な取り出しにもやや冗長でした。
新しい手続き型 API を使った書き方
import std.sumtype;
import std.stdio;
alias Value = SumType!(int, string); // int と string のいずれかを保持する型
Value v = 123;
// has!T で確認
if (v.has!int)
{
writeln("整数: ", v.get!int);
}
else if (v.has!string)
{
writeln("文字列: ", v.get!string);
}
// tryGet!T を使う例(try-catchによる処理)
try
{
writeln("値: ", v.tryGet!string); // string ではないので例外
}
catch (Exception e)
{
writeln("例外発生: ", e.msg);
}
内容まとめると以下3点です。
-
has!T
:型チェック用。軽い条件分岐に。 -
get!T
:型が一致することを前提に取り出す。簡潔だが要注意。 -
tryGet!T
:型不一致を例外で検出。安全だが多少重い。
これらの新しい API により、SumType の使い勝手がさらに向上し、より幅広いスタイルのコードに適用できるようになりました。
match
を使った関数型スタイルが少しとっつきにくいな、という方にもおすすめできるようになったので、ぜひ使ってみてください!
std.uni
が Unicode 15.1.0 から Unicode 16.0.0 にアップグレードされました。
std.uni
モジュールが更新され、Unicode 16.0.0 に対応しました。
この std.uni
は、D言語における Unicode 処理の中心を担う標準ライブラリで、文字のカテゴリ判定(例: アルファベットかどうか)、正規化、ケース変換、書記素境界の判定など、多くの Unicode 関連処理を提供しています。
Unicode は年に1回程度更新される仕様で、新しい文字の追加や既存文字の定義変更、スクリプト(文字体系)の追加、カテゴリ分類の見直しなどが含まれます。
Unicode 16.0 の詳細については公式ページも参照ください。
特に国際化対応(i18n)や絵文字・多言語対応UIを作成している場合は、最新のUnicode仕様に対応していることが重要です。
現時点で std.uni
が使用している Unicode バージョンは、以下のようにソースを確認するか、ドキュメントを見ることで確認できます。
import std.uni;
pragma(msg, "Unicode version used: ", unicodeVersion); // 16.0.0
今後の Unicode 更新においても、D 言語で正しく文字を扱うためには定期的な追従が重要です。今回のアップグレードもその一環として重要な意味を持つリリース内容です。
dub変更点
cImportPaths
が dmd と ldc で動作しない問題を修正しました。
dub.sdl/dub.json
の設定項目である cImportPaths
が、D言語の主要コンパイラであるDMDおよびLDCで正しく機能しない問題が修正されました。
元々 cImportPaths
は、2023年6月版(dmd 2.104.0)で追加された機能なのですが、過去の動作検証では使えていたと思うので修正内容がちょっと良く分かりませんでした。(動いてなかった?)
dlang.org変更点
改善点ですが、あまり知られていない仕様がいくつかドキュメントになったようなのでピックアップしてみました。
Bugzilla 19348: 構造体キャストについてのドキュメントが不足している
src: https://issues.dlang.org/show_bug.cgi?id=19348
link: https://dlang.org/spec/expression.html#cast_struct
構造体はサイズが合っていればキャストできる、という仕様です。
以下リトルエンディアンでの例になります。
struct S
{
int i;
}
struct R
{
short[2] a;
}
S s = cast(S) 5; // S(5) と同じ
assert(s.i == 5);
static assert(!__traits(compiles, cast(S) long.max)); // S(long.max) は無効
R r = R([1, 2]);
s = cast(S) r; // ビットパターンを維持して型変換
assert(s.i == 0x00020001);
byte[4] a = [1, 0, 2, 0];
assert(r == cast(R) a); // ビットパターンを維持して型変換
Bugzilla 24868: 構造体から静的配列へのキャストがドキュメント化されていない
src: https://issues.dlang.org/show_bug.cgi?id=24868
link: https://dlang.org/spec/expression.html#cast_struct
上記の例の延長線上ですが、構造体は同じサイズの静的配列にキャストできます。
struct S { short a, b, c; }
S s = S(1, 2, 3);
static assert(!__traits(compiles, cast(short[2]) s)); // size mismatch
short[3] x = cast(short[3]) s;
assert(x.tupleof == s.tupleof);
auto y = cast(byte[6]) s;
assert(y == [1, 0, 2, 0, 3, 0]);
Bugzilla 24876: スライスから静的配列へのキャストがドキュメント化されていない
src: https://issues.dlang.org/show_bug.cgi?id=24876
link: https://dlang.org/spec/expression.html#cast_array
これも「サイズが同じならキャストできる」のルールに従ったキャストのルールです。
動的配列は一般にサイズが不明ですが、スライスはサイズが分かるので、スライスから静的配列へのキャストは可能となっています。
char[4] a;
static assert(!__traits(compiles, a = cast(char[4]) b)); // unknown length
static assert(!__traits(compiles, a = cast(char[4]) b[0..2])); // too many bytes
a = cast(char[4]) b[0..1]; // OK
const i = 1;
a = cast(char[4]) b[i..2]; // OK
非推奨または廃止される機能
今回は、非推奨化が2件、新たに廃止やエラーとなるものは8件でした。
また今後の予定などは以下のページにまとまっていますので、こちらも参考としてみてください。
非推奨
フィールドを自身で初期化することが非推奨となりました。
構造体やクラスのフィールドを、自分自身の値で初期化するコードが非推奨になりました。
多くの場合、コンストラクタの引数などで初期化するわけなので、自身のフィールドを再代入することは望んでいないはずです。期待通り初期化したような気になってしまうのは危険なので、今回エラーになったということです。
struct S
{
int field;
this(int feild) // this(int field) と書いたつもりだが誤字
{
this.field = field; // 意図せず自分自身の再代入で初期化している
}
}
source\app.d(248,4): Deprecation: cannot initialize field `field` with itself
this.field = field; // 意図せず自分自身の再代入で初期化している
^
source\app.d(246,3): did you mean to use parameter `feild`?
this(int feild) // this(int field) と書いたつもりだが誤字
^
エラーメッセージも親切で、パラメーターの名前を間違えたのでは?と教えてくれます。
対処としては、誤字を修正するか、メッセージ通りで問題ないでしょう。
型安全な可変長クラスパラメータが非推奨となりました。
影響度は大きくないと思いますが、ちょっと気になる機能の非推奨化です。
具体的なコードは以下の通りですが、この機能はある種の**暗黙的なクラスの生成(implicit construction)**を可能にしていました。
void check(bool x, Exception e...) {
if (!x)
throw e;
}
check(false, "oops"); // "oops" は Exception ではない
可変長引数で、よく見るコードは恐らくこうですね。
void check(bool x, Exception[] e...) {
if (!x)
throw e[0];
}
check(false, new Exception("oops"), new Exception("oops2")); // Exception を1つ以上渡す
そもそも前者はどう動くのかよく知らないのですが、ChangeLogでも「混乱を招く」との理由で廃止非推奨とするそうです。
これらのコードを書くと、次のような警告が発生します。
source\app.d(255,7): Deprecation: typesafe variadic parameters with a `class` type (`Exception e...`) are deprecated
void check(bool x, Exception e...) {
^
source\app.d(255,7): Deprecation: typesafe variadic parameters with a `class` type (`Exception e...`) are deprecated
void check(bool x, Exception e...) {
^
クラスと可変長引数がNG、ということでした。
つまり構造体と組み合わせる場合はエラーが出ません。クラスは意図せず new
することになるわけで、それは暗黙的にやるのはやりすぎ、やっぱり避けたいということですね。
修正方法としては2通り考えられます。
方法1: 呼び出し側でインスタンスを明示的に生成する
void check(bool x, Exception e)
{
if (!x)
throw e;
}
void main(string[] args)
{
check(args.length > 1, new Exception("missing argument"));
}
これはもっとも標準的なやり方です。
元々の動作もこちらに近いようですし、特に関数が @nogc
であれば内部で new
ができないので外から渡してください、ということですね。
方法2: 呼び出し先でコンストラクタを使う
void check(bool x, string msg)
{
if (!x)
throw new Exception(msg);
}
void main(string[] args)
{
check(args.length > 1, "missing argument");
}
この方法では、引数は文字列のまま渡し、関数内で new Exception
に変換します。
どれくらい関数を呼び出しているかによりますが、こちらの方が修正は少なく済むかもしれません。
引数の型と違うものを渡せるというのがちょっと不思議な機能でしたが、今回より適切な振る舞いになるので警告を見かけたら修正してみてください。
廃止/エラー
criticalRegionLock
は削除されました。
何やら内部処理に近い部分で、あまり影響を受ける方はいないと思われる変更です。
D言語のランタイムから、criticalRegionLock
によるクリティカルリージョン制御機能が削除されました。
これは thread_enterCriticalRegion
/ thread_exitCriticalRegion
によって スレッドの一時停止を防ぐ「重要区間(クリティカルリージョン)」を明示する機構でした。
しかし、この機能は深刻な設計上の欠陥があり、実際には使われていないことも判明したため、修正ではなく削除という判断が取られました。
この機能の本来の目的は、GC(ガベージコレクタ)の「stop-the-world」動作中にスレッドが重要な処理を完了するまで待つというものでした。つまり、あるスレッドが thread_enterCriticalRegion
を呼び出した後は、thread_exitCriticalRegion
を呼ぶまで 強制停止(suspend)されないことを保証するという仕組みです。
ですが、内部で利用していた criticalRegionLock
の設計には競合状態(レースコンディション)を引き起こすバグがあったそうです。
この挙動は、通常のアプリケーションではほとんど発生しませんが、悪意あるユーザーが入力を使って強制的に再現させることも理論的には可能で、これはセキュリティ的な脆弱性にもつながる可能性があるため、今回急遽削除されたというものになります。
実際利用者に影響はあるのかというと、ごく一部の非常にローレベルな処理をしている場合を除いて影響はありません。
この機能は、一般のアプリケーションやライブラリコードでは通常使われておらず、内部実装の失敗だったと見るべき機能です。
安全性向上の観点から見ても、削除されたことでむしろ好ましいと言えるでしょう。
補足:そもそも何のための機能だったのか?
この仕組みの元ネタは、Mono(.NET互換環境)の SGen GC が採用していた「critical region」管理で、特定のスレッド処理をGCが中断しないよう制御するものでした。
ただし、D言語のスレッド管理やGCモデルとは相性が悪く、実際には使いこなせる設計ではなかったということのようです。
これらの機能を使いこなす方々であれば修正方針も目途が付くと思いますが、安全になったということでコンパイラの更新は是非検討いただければと思います。
キーワード auto
と ref
は、必ず隣接して記述する必要があります。
これは一見地味ですが、コードの読みやすさ・意図の明確さのために重要な変更です。
今回から、関数テンプレートやローカル変数の宣言などで auto
と ref
を使う場合は、必ず隣り合わせ(auto -> refの順)で書く必要があるというルールが導入されました。
つまり、今までは以下のように書いても動いていたコードが、今後はエラーになります。
void t()(ref const auto int x) // 非推奨(Deprecation)
{
ref auto y = x; // エラー(Error)
}
発生するエラーは以下のようなものです。
source\app.d(267,12): Error: variable `y` - `auto ref` variable must have `auto` and `ref` adjacent
ref auto y = x; // エラー(Error)
^
source\app.d(271,6): Error: template instance `app.test16.t!()` error instantiating
t!()(n); // エラー(Error)
今回の auto ref
という隣接ルールですが、参照変数の導入による必要な措置のようです。
構文敵にあいまいさは少ない方が良いので、見かけたらパラメーターの順序を修正してみてください。
修正例
// 関数テンプレートの場合:
void print(T)(auto ref const T x) {
writeln(x);
}
// ローカル変数の場合:
auto ref y = x;
delete
キーワードが廃止されました。
ついにと言いますか、長い時間をかけて進められていた変更がついに完了しました。
delete
キーワードが廃止され、識別子として自由に使用できるようになりました。
D言語では長らく、C++風に delete
キーワードを使ってヒープオブジェクトを解放する構文がサポートされていました。
auto obj = new Object();
delete obj; // 古い書き方
しかしこの方法は、GC(ガベージコレクタ)を標準とするD言語のメモリ管理スタイルにそぐわず、誤解やクラッシュの原因になりやすいため、以前から非推奨とされていました。どうしても破棄したい場合は代替として destroy()
関数が用意されており、そちらの使用が推奨されてきました。
エラーとなるコード例:
class MyClass {}
void main() {
auto obj = new MyClass();
delete obj; // ここがエラーになる
}
このようなコードは、コンパイル時に次のようなエラーが発生します。
source\app.d(279,12): Error: undefined identifier `delete`, did you mean function `deleteme`?
delete obj; // ここがエラーになる
^
source\app.d(279,5): Error: declaration `app.test17.obj` is already defined
delete obj; // ここがエラーになる
^
source\app.d(278,10): `variable` `obj` is defined here
auto obj = new MyClass();
^
エラーメッセージではもうすっかり delete
のことは忘れてしまったようで、代わりに「 datetime
ですか?」というメッセージが表示されます。(違うよ)
destroy()
ですか?と聞いてくれると親切なのですが、そのうち改善される事を期待しましょう。
正しい対処方法
D言語で手動的にデストラクタを呼び出したい場合は、destroy()
を使用してください。
destroy()
は object
モジュールで定義されているので、何も import
せずに使えます。
auto obj = new MyClass();
destroy(obj); // 明示的にデストラクタを呼ぶ(メモリはGCが回収)
恩恵: delete
を識別子として使用できる
今回の変更により、delete
は予約語ではなくなったため、以下のように変数名・関数名・enum名などとして自由に使えます:
enum Action
{
add,
delete // 以前はエラーだったが、今はOK
}
void delete(T)(T obj) {
// 関数名として使える
}
こういった柔軟性は、ライブラリの設計やDSL風記法の際に役立ちますね。
SQL関連のライブラリなどでは、delete
を使うことが多いので、これでより自然なコードが書けるようになると思います。
長らく非推奨で delete
を使うことも少ないと思うので、影響を受ける方も少なそうですが、エラーに遭遇した際は、destroy()
への置き換えを基本として対応してください。
複数の値を持つ case
でのフォールスルーがエラーとして扱われるようになりました。
従来は非推奨(Deprecation)扱いだった「複数値の case
から他の case
へフォールスルーする構文」が、今回から明確にエラーとして扱われるようになりました。
D言語の switch
文では、1つの case
にカンマ区切りで複数のラベルを記述できます。
switch (value)
{
case 0, 1:
// value が 0 または 1 の場合に実行される
doSomething();
break;
}
ここまでは問題ありませんが、この case 0, 1:
から default:
や別の case:
へフォールスルー(break や goto
を挟まずに次の case:
に続けて処理を書く)すると、今後はコンパイルエラーになります。
というわけで以下エラーの例です。
int i;
switch (0)
{
case 0, 1:
i = 20;
default:
assert(0); // エラー:フォールスルー禁止
}
このコードは、case 0, 1:
にマッチしたあと、明示的な break
が無いために default:
にフォールスルーしています。以前は警告(Deprecation)で済んでいましたが、今回からは以下のようなエラーになります:
source\app.d(290,3): Error: switch case fallthrough - use 'goto default;' if intended
default:
^
case
が続く場合はエラーメッセージが変わるのでこちらも確認しておきます。
int i;
switch (0)
{
default:
case 0, 1:
i = 20;
case 2, 3:
i = 30; // エラー:フォールスルー禁止
}
こちらも同様に、case 0, 1:
にマッチしたあとに case 2, 3:
へ処理が続いており、フォールスルーとみなされてエラーになります。
source\app.d(304,3): Error: switch case fallthrough - use 'goto case;' if intended
case 2, 3:
^
Fallthroughを前提としている場合の対処方法は、「明示的に goto case;
/ goto default;
を書く」となります。
以下のような形です。
switch (0)
{
case 0, 1:
i = 20;
goto default; // Fallthroughを明示
default:
assert(0);
}
switch (0)
{
default:
case 0, 1:
i = 20;
goto case; // Fallthroughを明示
case 2, 3:
i = 30;
}
フォールスルーを意図していない場合は、単純に break
を挟むのが対処になります。
switch (0)
{
case 0, 1:
i = 20;
break;
default:
assert(0);
}
これらのエラーを見かけたら、ぜひ意図を明示する形に修正してみてください。
フィールドのデストラクタがより緩い属性を持つ場合、コンストラクタでエラーが発生するようになりました。
今回から、構造体やクラスのフィールドが持つデストラクタ(~this()
)よりも、現在のコンストラクタが「制約の厳しい属性(pure / nothrow / @nogc
/ @safe
など)」で定義されている場合、そのコンストラクタでエラーとなるようになりました。(制約の緩い構造体をフィールドに持つ場合)
コードがわかりやすいので、早速見ていきましょう。
以下のコードはすべて、コンストラクタに属性を付けているのに、フィールドのデストラクタにはその属性がないため、エラーとなります。
struct HasDtor {
~this() {} // 属性なし
}
struct Pure {
HasDtor member;
this(int) pure {} // エラー:pure に合わない
}
struct Nothrow {
HasDtor member;
this(int) nothrow {} // エラー:nothrow に合わない
}
struct NoGC {
HasDtor member;
this(int) @nogc {} // エラー:@nogc に合わない
}
struct Safe {
HasDtor member;
this(int) @safe {} // エラー:@safe に合わない
}
これで発生するエラーは以下のようなものです。
source\app.d(317,3): Error: `app.test20.Pure.this` has stricter attributes than its destructor (`pure`)
this(int) pure {} // エラー:pure に合わない
^
source\app.d(317,3): The destructor will be called if an exception is thrown
source\app.d(317,3): Either make the constructor `nothrow` or adjust the field destructors
source\app.d(317,3): Error: field `member` must be initialized in constructor, because it is nested struct
this(int) pure {} // エラー:pure に合わない
^
source\app.d(322,3): Error: field `member` must be initialized in constructor, because it is nested struct
this(int) nothrow {} // エラー:nothrow に合わない
^
source\app.d(327,3): Error: `app.test20.NoGC.this` has stricter attributes than its destructor (`@nogc`)
this(int) @nogc {} // エラー:@nogc に合わない
^
source\app.d(327,3): The destructor will be called if an exception is thrown
source\app.d(327,3): Either make the constructor `nothrow` or adjust the field destructors
source\app.d(327,3): Error: field `member` must be initialized in constructor, because it is nested struct
this(int) @nogc {} // エラー:@nogc に合わない
^
source\app.d(332,3): Error: `app.test20.Safe.this` has stricter attributes than its destructor (`@system`)
this(int) @safe {} // エラー:@safe に合わない
^
source\app.d(332,3): The destructor will be called if an exception is thrown
source\app.d(332,3): Either make the constructor `nothrow` or adjust the field destructors
source\app.d(332,3): Error: field `member` must be initialized in constructor, because it is nested struct
this(int) @safe {} // エラー:@safe に合わない
^
ちょっとごちゃごちゃしていますが、要するに「this
の属性がフィールドのデストラクタよりも厳しい」というメッセージです。
特に「app.test20.Pure.this
has stricter attributes than its destructor (pure
)」のあたりを読むと、this
の属性が pure
であることがわかります。
pure
は「副作用のない関数」という意味ですが、フィールドのデストラクタは pure
ではないため、エラーとなっている、という感じです。
対処方法
属性付きコンストラクタを使いたい場合は、フィールドのデストラクタにも同じ属性を付与する必要があります。
struct HasDtor {
~this() pure nothrow @nogc @safe {} // 属性をすべて付ける
}
struct S {
HasDtor member;
this(int) pure nothrow @nogc @safe {} // OK
}
また、フィールドの型を持つ構造体やクラスが他人の定義(たとえば外部ライブラリ)で、属性が変更できない場合は、コンストラクタ側の属性を削除または緩和する必要があります。
struct HasDtor {
~this() {} // 属性なし
}
struct S {
HasDtor member;
this(int) {} // 属性なしに変更すればOK
}
影響は比較的限定的かと思いますが、構造体の設計時には意識しておきたいポイントです。エラーが出た場合は、フィールドのデストラクタ定義を見直してみてください。
異なる型のポインタ同士の引き算がエラーとして扱われるようになりました。
これまでD言語では、異なる型のポインタを -
演算子で引き算してもコンパイルエラーにはならず、暗黙のキャストや型変換に頼ってそのまま計算されることがありました。
今回の変更により、異なる型のポインタ同士を直接引き算するコードは明確にエラーとして扱われるようになりました。
エラーとなるコード例
void test()
{
auto diff1 = (ushort*).init - (ubyte*).init; // NG
auto diff2 = cast(void*)8 - cast(int*)0; // NG
auto diff3 = cast(int*)8 - cast(void*)0; // NG
}
これらのコードでは、ushort*
と ubyte*
、void*
と int*
のように、ポインタ型が異なるにもかかわらず引き算を行っており、ポインタ差の結果(ポインタ間の距離)を求めようとしています。
この操作はDの型安全性の観点から好ましくなく、現在では以下のようなエラーが発生します。
source\app.d(338,18): Error: cannot subtract pointers to different types: `ushort*` and `ubyte*`.
auto diff1 = (ushort*).init - (ubyte*).init; // NG
^
source\app.d(339,18): Error: cannot subtract pointers to different types: `void*` and `int*`.
auto diff2 = cast(void*)8 - cast(int*)0; // NG
^
source\app.d(340,18): Error: cannot subtract pointers to different types: `int*` and `void*`.
auto diff3 = cast(int*)8 - cast(void*)0; // NG
^
対処方法
異なる型のポインタ同士の差を取りたい場合は、同じ型にキャストしてから演算を行う必要があります。
以下のように cast(void*)
などで明示的に統一しましょう。
// 両方 void* にキャストすることで OK
auto diff1 = cast(ptrdiff_t)(cast(void*)8 - cast(void*)0); // OK: 生のアドレスの差
// または、同じ型(例:ubyte*)にそろえる
auto diff2 = cast(ptrdiff_t)(cast(ubyte*)8 - cast(ubyte*)0); // OK
このように書けば、要素数としてではなく「アドレス差(バイト数)」として安全に扱えます。
ポインタ差を取る際は、同じ型のポインタに揃えるか、void にしてアドレス単位で扱う*のが推奨されます。
エラーを見かけたら、キャスト忘れがないかチェックして、型を揃えたコードに修正してみてください。
nothrow
関数の in/out
契約が例外をスローする場合、エラーが発生するようになりました。
今回の変更により、関数に nothrow
属性が付いている場合、その in
/ out
契約内で例外をスローする(throw
を含む)コードはエラーとなるようになりました。
従来は警告(Deprecation)にとどまっていましたが、今後は明確にコンパイルエラーとなります。
void test() nothrow
in {
throw new Exception("bad input"); // エラー
}
out {
throw new Exception("bad output"); // エラー
}
do {
}
このようなコードでは、以下のようなエラーが発生します。
source\app.d(352,6): Error: `app.test`: `in` contract may throw but function is marked as `nothrow`
void test() nothrow
^
source\app.d(352,6): Error: `app.test`: `out` contract may throw but function is marked as `nothrow`
void test() nothrow
^
D言語では、関数に nothrow
を付けることで、「この関数は絶対に例外をスローしない」とコンパイラに保証します。最適化や安全性のための重要な属性です。
しかしこの制約は、関数本体だけでなく、前後条件を記述する in
/ out
ブロックにも及びます。
一方で、Dの例外階層はやや複雑です。Throwable
を親クラスとして、ユーザーコード向けの Exception
と、システムエラーなどを表す Error
に分かれています。
-
class Throwable
:すべての例外の基底クラス -
class Exception : Throwable
:ユーザーコードでスロー可能な例外(対処により復旧可能性がある状況) -
class Error : Throwable
:システムエラーや致命的なエラーを表すクラス(通常プログラムが続行できないパニック的な状況)
ユーザーコードからは基本的に Exception
を throw
することになりますが、これは明示的な例外スローに該当し、nothrow
制約に違反します。
これに対して、assert
が発生させる AssertError
は Error
の一種であり、nothrow
関数内でも in
/ out
契約でスロー可能とされています。
つまり、契約違反の検出には throw
ではなく assert
を使うのが正しい書き方ということになります。
nothrow
関数の契約内で throw
を使っていた場合は、以下のいずれかの方法で修正できます。
方法1: throw
を assert
に置き換える(推奨)
契約違反は本来、バグとして即座に発見すべきものなので、assert
の使用が自然です。
void test() nothrow
in {
assert(0, "bad input"); // OK:Error をスロー
}
out {
assert(0, "bad output"); // OK
}
do {
}
この方法であれば nothrow
制約を維持できます。
方法2: 関数から nothrow
を外す
どうしても Exception
をスローする必要がある場合は、関数定義から nothrow
を削除します。
void test()
in {
throw new Exception("bad input"); // OK(nothrow なしなら)
}
do {
}
ただし、この方法は関数の利用箇所で nothrow
を前提としたコード(例:@nogc
や pure
と併用している箇所など)に影響を与える可能性があるため、注意が必要です。
内容をまとめると以下3点がポイントです。
-
nothrow
関数では契約ブロック内でもthrow
禁止 -
in
/out
契約内での検証にはassert
を使う(AssertError
は許容される) -
Exception
をスローしたい場合はnothrow
を外すしかない
今回の変更は、安全性と属性の一貫性を高めるためのものであり、契約と関数属性の設計がより明確に分離されるようになったと言えます。
エラーを見かけたら、まずは assert
への置き換えを検討してみてください。
debug
または version
ステートメントで使用される整数が言語仕様から削除されました。
これは比較的レアケースですが、debug
や version
ステートメントで整数リテラル(数字)を使う記法が、今回正式に言語仕様から削除されました。
従来のD言語では、以下のように debug(1)
や version(42)
のように整数を使ってビルド条件を切り替えることができていました。
debug(1) {
writeln("Debug level 1");
}
version(42) {
writeln("Custom version 42");
}
しかしこのような整数による条件分岐は、可読性や管理性が悪く、何の意味を持つかが分かりづらいため、以前(dmd 2.101.0)から非推奨(Deprecated)となっていました。
そのため、今後は整数を使った debug
や version
の指定はコンパイルエラーになります。
エラーとなるコード例
debug(1) {
writeln("This is debug level 1");
}
version(100) {
writeln("This is version 100");
}
このコードをコンパイルしようとすると、次のようなエラーが出ます。
source\app.d(361,8): Error: identifier expected inside `debug(...)`, not `1`
debug(1) {
^
source\app.d(365,10): Error: identifier expected inside `version(...)`, not `100`
version(100) {
^
識別子が必要であることを示すエラーメッセージが表示されます。
identifier expected inside debug(...)
とあるように、整数の代わりに「識別子」を使う必要があることがわかります。
というわけで、メッセージの通りに直す必要があります。
対応: 分岐箇所で、識別子を使って分岐
debug(logging) {
writeln("Logging enabled");
}
version(customFeature) {
writeln("Custom feature enabled");
}
対応: 識別子の設定
「コード内でグローバルに定義する方法」と「コンパイル時にコマンドラインオプションで指定する方法」の2つがあります。
コード内で設定する場合は以下のようになります。
debug = logging;
version = customFeature;
dmd
コマンドでは以下のように指定します。
dmd -debug=logging
dmd -version=customFeature
dub.sdl
などの設定ファイルでは、以下のように指定すると -debug=<identifier>
や -version=<identifier>
と同等になります。
debugVersions "logging"
versions "customFeature"
もしエラーが出たら、まずは debug(数字)
/ version(数字)
のような書き方を探して、上記の内容を識別子ベースへ書き換えてみてください。
まとめ
今回は本当に大量の機能強化がありました。
前回のリリースが遅れたおかげで止まっていた内容が一気に放出されています。
ざっくり紹介の内容だけで44件、小分けにすると1人アドベントカレンダーができる規模です。
ひとまず非推奨や廃止エラー系の話題も大きな影響は無さそうですし、エラーメッセージの改善などがあるのでぜひアップデートしてみてください。
今回も様々な新機能が出ていますし、今後も強化予定がありますので、引き続き便利に使いながら今後のリリースを期待していきましょう!