はじめに
D言語の公式コンパイラである dmd が更新され、最新バージョンである 2.099.0
が 2022/03/06 にリリースされました。
今回は前回から4ヶ月半ほど経ってのリリースとなり、前々回から前回にかけてと同程度の間隔が開いたことになります。
期間相応で多数の変更があり、箇条書きの部分は正直読み切れないレベルです。
新機能も複数あるほか地味に嬉しい変更が見られるので、個人的に気になった部分の抜粋となりますがトピックご紹介していきます。
より細かい内容は下記リンクからChangeLogをご覧いただけます。
- ChangeLog
また、前回である昨年10月のリリースまとめは以下になります。
- D言語の更新まとめ 2021年10月版(dmd 2.098.0)
変更点目次
コンパイラ変更点
- 引数に指定された
ref scope return
属性がより厳密に判定されるようになります。 - コンパイラに
__traits(parameters)
が追加されました。 - ImportCにモジュールをインポートする機能が追加されました。
- 互換性のあるシーケンス間のキャストが追加されました。
- 関数ごとにアセンブラコードを出力するコマンドラインスイッチ
-vasm
が追加されました。 -
-preview=intpromote
スイッチがデフォルトで設定されるようになります。 - Windows をターゲットとした場合、
-m32
により MS Coff オブジェクトが生成されるようになります。 - 非 root モジュールの
unittests
が無視されるようになります。 -
main
がnoreturn
型を返すようになり、戻り値の推論がサポートされました。 -
switch case
のフォールスルーはエラーになるように変更されました。 - DIP 1034で提案された
throw
式が実装されました。 - 初期化子を取得するための
__traits(initSymbol)
の追加されました。
改善点
- Bugzilla 5096: 非対称の括弧に対するより読みやすいエラー
- Bugzilla 7925:
extern(C++)
のデリゲートとは - Bugzilla 11008: ユーザー定義の
main
関数が存在する場合でも-main
スイッチを許可するようにします。 - Bugzilla 20340:
[betterC]
-main
は betterC でも Dのメイン関数を挿入します。 - Bugzilla 20616:
Error: undefined identifier __dollar
- Bugzilla 21160: DWARF:
DW_AT_main_subprogram
は_Dmain
に対して発行される必要があります。 - Bugzilla 22113:
main
関数の型としてnoreturn
を許可します。 - Bugzilla 22198: 静的配列のコンパイル時境界チェック
- Bugzilla 22278:
[条件付きコンパイル]
in
とout
のフラグが必要です。 - Bugzilla 22291: 関数の引数のタプルを返す
__traits(arguments)
(訳注__traits(parameters)
のことです) - Bugzilla 22353: ヘッダー生成で、属性宣言の末尾に空白が発生します。
- Bugzilla 22354: ヘッダー生成で enum 宣言の末尾に空白が発生します。
- Bugzilla 22355:
mscoff
の LLD フォールバックは、いくつかの古い VS バージョンの存在下で壊れています。 - Bugzilla 22377: Windows で
extern(C++)
mangling ICE の場所を表示するようにします。 - Bugzilla 22379: OpenBSD: バックトレースシンボルを取得するために
-lexecinfo
をリンクします。 - Bugzilla 22419:
main
の戻り値の推論を可能にする。 - Bugzilla 22423: DWARF
DW_TAG_subprogram
はDW_AT_decl_column
を生成する必要があります。 - Bugzilla 22426: DWARF
DW_AT_noreturn
は、関数がnoreturn
であるときに存在すべきです。 - Bugzilla 22459: DWARF: デリゲートの型名は区別できるようにすべきです。
- Bugzilla 22468: DWARF:
dchar
型のエンコーディングが欠落しています。 - Bugzilla 22469: DWARF: いくつかのデバッグ情報の型が間違った名前になっています。
- Bugzilla 22471: DWARF: 生成されたメインが
DW_AT_artificial
としてマークされていない - Bugzilla 22494:
dmd.conf
の検索パスがdmd man
のページから抜けています。 - Bugzilla 22508: DWARF: 連想配列は
_AArray__
の代わりに修飾名を報告すべきです。 - Bugzilla 22519: DIP1000:
ref return
のアドレスを取得できません。 - Bugzilla 22541: DIP1000:
ref-return-scope
パラメータのあいまいさを解決します。 - Bugzilla 22631: ImportC: C++11の
unscoped enum
を基礎型としてサポートします。 - Bugzilla 22672:
ValueSeq
を互換性のあるTypeTuple
にキャストできるようにしました。 - Bugzilla 22733:
hdrgen
は~this()
の STC 属性の順序に矛盾を生じさせます。 - Bugzilla 22746:
nothrow
とマークされたthrow
を持つ関数が不正なエラーを発生させます。 - Bugzilla 22753: インポートモジュールの非推奨メッセージは、メッセージがない場合、
hifen
を生成すべきではありません。 - Bugzilla 22754: ヘッダー生成で可視属性宣言の末尾に空白が発生します。
ランタイム変更点
- OpenBSD の
ioctls
についてサポートが追加されました。 -
@safe class
のopEquals
についてサポートが追加されました。
改善点
- Bugzilla 14892:
-profile=gc
が GC API の割り当てを考慮していません。 - Bugzilla 20936:
core.sync.rwmutex
はshared
のオーバーロードを持つべきです(そして@safe
コードで使用可能にすべき)。 - Bugzilla 21005: 連想配列の
hashOf
を高速化しました。 - Bugzilla 21014:
aa.byKeyValue
,byKey
,byValue
のドキュメントが非常に不足しています。 - Bugzilla 22378: OpenBSD:
execinfo.d
とunistd.d
がインストールされていません。 - Bugzilla 22669: OpenBSD:
socket.d
を同期します。 - Bugzilla 22670: *BSD の
kqueue-backed
API 互換のinotify shim
ライブラリをサポートします。
ライブラリ変更点
-
checkedint
をexperimental
の外に移動します。 - Forward Range に対する
chunkBy @safe
とsplitWhen
が完全に@safe
になります。 -
std.csv
は、オプションで列数が可変の csv ファイルを扱えるようになります。 -
std.experimental.logger
のログレベルの既定値がLogLevel.warning
に変更されます。 -
std.conv.to
がstd.typecons
のタプルを受け入れるようになりました。
改善点
- Bugzilla 13551:
std.typecons
のtuple
に対してstd.conv.to
が動作するようにします。 - Bugzilla 17488:
getTempDir()
がプラットフォームごとに異なる動作をする - Bugzilla 18051:
formattedRead
/unformatValue
でenum
のサポートが欠落している。 - Bugzilla 21507:
SysTime.toISOExtString
はロギングや一貫したファイル名の作成に使用できません。 - Bugzilla 22117:
SumType
にスコープポインタを格納できない。 - Bugzilla 22340:
totalCPUs
がより正確な CPU 数を返すようにします。 - Bugzilla 22370:
std.concurrency.spawn*
はnoreturn
な呼び出し可能オブジェクトを受け入れるべきです。 - Bugzilla 22511: テンプレート化された型が精巧なコピーコンストラクタを持っている場合、
Nullable
がコピー可能でない。 - Bugzilla 22532:
std.experimental.logger
デフォルトのログレベルをLogLevel.warning
, またはLogLevel.off
に変更する。(訳注LogLevel.warning
に変更された件です) - Bugzilla 22701:
std.typecons.apply
は述語が呼び出し可能かどうかを不必要にチェックしています。
DUB 変更点
- Windows: 実行ファイルやDLLと共にPDBファイルを
targetPath
にコピーするようにします。
インストーラー変更点
改善点
- Bugzilla 18362: dmdを
LTO
とPGO
を有効にしてビルドしたものに置き換えます。 - Bugzilla 22078: install.sh: ARM64 をアーキテクチャとして認識するようになります。
dlang.org 変更点
改善点
- Bugzilla 22425: 配列の暗黙変換についてのドキュメントが不十分です。
- Bugzilla 22431: OpenBSD をサードパーティのダウンロードリストに追加します。
注目トピック
言語・コンパイラ変更点
変更 : 引数に指定された ref scope return
属性がより厳密に判定されるようになります。
こちらは引数に対する属性付与に関する変更です。正直ここまで厳密にやるだろうか?という疑問は若干あり、影響がある方は少ないだろうと思います。
ここで対象となる属性を少しご説明しておくと、 ref
は参照引数、scope
はスコープ外への持ち出し不可、をそれぞれ表しています。
そこに加えて return
があり、 ref
や scope
と組み合わせて使うと戻り値に対する特殊な意味を持たせることができます。
今回これが厳密化されるというのは、元々の言語機能として return ref
という指定と return scope
という指定の2種類が存在することにより、 ref scope return
と書いたらどうなるのか?という仕様が正確に定められた、という意味合いになります。
ひとまず結論から言えば、 ref scope return
の return
修飾は ref
に対して付与されることになります。つまり return ref scope
と書いたのと同じです。(正直エラーで良いんじゃないの?と思いましたが)
また、 return
が隣接しているものが優先されるということで、return ref scope
や return scope ref
、 scope return ref
などと書けば return
の右隣に書かれたものと結合します。
仕様が少し厳密になった程度ではありますが、変更によって挙動が変わるコードが存在する可能性もゼロではありませんので、こういった記述を使っているコードがあれば動作を確認しておくと良いかと思います。
その他、return ref
と return scope
についてはそれぞれ以下のリンクにその動作の詳細がありますので、気になった方は参照してみてください。
- https://dlang.org/spec/function.html#return-ref-parameters
- https://dlang.org/spec/function.html#return-scope-parameters
強化 : コンパイラに __traits(parameters)
が追加されました。
今回入ったメタプログラミング機能の目玉の1つです。
グローバルな関数やクラスのメソッドにおいて、その引数を表すタプルが取得できる機能です。
たとえば2つの引数を受け取る関数を明示的に宣言したあとでも、「何番目」という形で番号を指定して引数を扱うことができるようになったりします。
void foo(ref int a, ref int b)
{
__traits(parameters)[0] += 1; // a += 1
__traits(parameters)[1] += 2; // b += 2
}
今までは同じようなことをやりたい場合、以下のように可変長テンプレートとテンプレート条件を用いて書いたりしていましたが、これで幾分楽に書けるようになった、ということになります。
void foo(Args...)(Args args) if (Args.length == 2)
{
args[0] += 1;
args[1] += 2;
}
なお、これはローカル関数だとそのスコープが反映される動作になります。
int testNested(int x)
{
static assert(typeof(__traits(parameters)).length == 1);
int add(int x, int y)
{
static assert(typeof(__traits(parameters)).length == 2);
return x + y;
}
return add(x + 2, x + 3);
}
もしローカル関数内で外側の引数を使いたい場合、 std.meta
にある AliasSeq
を使って一度 alias
な変数に束縛しておくと良いです。
void test(int a)
{
import std.meta : AliasSeq;
alias outerArgs = AliasSeq!(__traits(parameters));
int nested(int x, int y)
{
return outerArgs[0] + x + y;
}
// 省略
}
補足
注意: foreach と opApply を組み合わせたときの動作は2.099.1で取り消されました。2.099.0用に記述は残しておきますが、バージョンアップで動きが変わるので注意してください
1点非自明な動作として foreach
と opApply
に関する挙動が規定されています。
何かといえば、以下のようなコードで __traits(parameters)
が opApply
のコールバックとして解釈されることにより、その引数を示してしまうというものです。
class Tree {
// 演算子オーバーロードの一種で、foreachの挙動を定めるための定義
int opApply(int delegate(size_t, Tree) dg) {
if (dg(0, this)) return 1;
return 0;
}
}
void useOpApply(Tree top, int x)
{
foreach(idx; 0..5)
{
// 静的な範囲に対する foreach であり、正しく useOpApply の引数を示している
static assert(is(typeof(__traits(parameters)) == AliasSeq!(Tree, int)));
}
foreach(idx, elem; top)
{
// top の opApply が適用されるため、そのコールバックで推論される引数を示している
static assert(is(typeof(__traits(parameters)) == AliasSeq!(size_t, Tree)));
}
}
なおこの場合も回避策としては、上記の AliasSeq
を使って foreach
の外で束縛してしまえば問題は回避できます。
そこまで使わない opApply
ですが、こういったことを起こす可能性はあるので、使いたい関数の冒頭で一度束縛してしまうのが安全かなと思います。
と、私含めメタメタなプログラミングをされる方向けの念のための注意点でした。
変更 : ImportCにモジュールをインポートする機能が追加されました。
ImportCについて軽く触れておくと、D言語コンパイラからC言語のコードを参照してコンパイルする機能のことです。
詳しくは前回更新の内容を確認ください。
今回の更新は、そのImportCで読み込まれる「C言語のソースコード」で #include
を書くのと同様、「D言語のソースコードを import
できるようになった」というものです。
記述としては以下のように __import
キーワードを利用します。 <>
で囲まないのがちょっと見慣れない感じでしょうか。
__import core.stdc.stdarg; // import a D source file
__import test2; // import an ImportC file
この機能によって、 D言語コンパイラ -> C言語ソースコード -> D言語ソースコード
という読み込み関係が成立するようになりました。
結果として、既存のC言語で書かれたソースをDから読んでコンパイルできるようにしてしまえば、「Cで書かれたコードの一部をDで書き直す」という選択が可能になります。
この機能が無ければ、Cで書かれたソースがImportCコンパイラで取り込めても、将来的に拡張するにはまたCを書くしかない、あるいは丸ごとDで書き直してImportCを脱するしかない、というわけでこの機能の登場です。
Cで書かれたコードの一部の関数をDで書き直す、一部のファイルをDで書き直す、といったことができるので書き換えのハードルが非常に低くなります。
実用性重視の言語だからこそ、機能の利用ハードルが下がるような取り組みが継続されているのは非常に好ましい流れかと思います。
強化 : 互換性のあるシーケンス間のキャストが追加されました。
シーケンスというのは聞き慣れないですが、ざっくり言えば「タプル」の言語的な正式名称?です。
一例を挙げると、以下のコードで宣言された変数のことを「シーケンス」と呼んでいます。
alias Seq(T...) = T;
void main() {
Seq!(int, int) a; // これで int の変数を2つ宣言したような状態。a[0] a[1] でアクセスできる。
}
で、今回これを一気にキャストすることができるようになりました。挙動は1つ1つキャストするのと同じです。
Seq!(int, int) a;
Seq!(short, long) b = cast(Seq!(short, long)) a;
またこれに合わせてライブラリ強化も行われており、std.conv
の to
テンプレートを使って機能的に近い Tuple
型に対しても同じ変換ができるようになります。
import std : Tuple, to;
Tuple!(int, int) a = tuple(10, -1000);
writeln(a); // Tuple!(int, int)(10, -1000)
Tuple!(short, long) b = a.to!(Tuple!(short, long));
writeln(b); // Tuple!(short, long)(10, -1000)
宣言自体は可変長テンプレート何かを使っていると無意識に使っていたりする機能ですが、いざキャストしたくなったときには結構便利かもしれませんね。
強化 : 関数ごとにアセンブラコードを出力するコマンドラインスイッチ -vasm
が追加されました。
アセンブラが見たいというのも結構特殊ですが、何がどう最適化されるのか調べたい人向け機能かもしれません。
たとえば以下のコードを書いて、
int demo(int x)
{
return x * x;
}
コンパイルするときに -vasm
フラグを付けます。
なお -c
はコンパイルのみを指定します。指定しないと main
がないのでエラーになります。
dmd test.d -c -vasm
そうすると、以下のようにアセンブリ表現が表示される、というものです。
_D4test4demoFiZi:
0000: 89 C8 mov EAX,ECX
0002: 0F AF C0 imul EAX,EAX
0005: C3 ret
必然的に -release
、-O
、-inline
あたりと組み合わせる事になるかと思いますが、
似たような機能が「Compiler Explorer」というサイトでも試せますので、こちらもおすすめです。
変更 : -preview=intpromote
スイッチがデフォルトで設定されるようになります。
サラッと書かれていますが、地味に重要な変更の1つです。
-preview=intpromote
が何をするスイッチかというと、整数型の昇格に関する挙動を切り替えるものです。
昇格というのは、計算で使われる値を一時的に拡大変換したりして結果精度を保証するための挙動のことです。
たとえば ubyte
という型は1バイトで 0 ~ 255
という値域を持ちますが、この値に単項マイナス演算子を使ったらどうなるか?という話です。
ubyte a = 255;
auto b = -a; // 結果はどうなる?型は?
で、今回からは -preview=intpromote
の効果で一貫してC言語と同じ振舞いになりました。
この例では結果は -255
という値になり、型は int
が正解です。
これが嫌で元の挙動に戻したい場合は、-revert=intpromote
というフラグを代わりに指定します。
そうすると以下のような警告が表示され、結果の値は 1
、型は ubyte
となります。
app.d(8): Deprecation: integral promotion not done for `-a`, remove '-revert=intpromote' switch or `-cast(int)(a)`
変更 : 非 root モジュールの unittest
が無視されるようになります。
非ルートな unittest
って一体なに?という話ですが、要するに標準ライブラリなどユーザーが書いてない参照先コードに含まれる単体テストです。
あるモジュールを単体テストしたいとき、通常はそのモジュールをコンパイルして単体テストするわけですが、現状の dmd はとても保守的にできており、標準ライブラリを含め参照先の単体テストも中身を解析してコード生成しようとする処理が行われていました。(結果テストされないコードなのですが)
これが極端に働く一例として、標準ライブラリにある Nullable
というテンプレートがあります。
これはその性質上利用頻度が高く様々な型でインスタンス化されること、加えてその時々で挙動が変わり得るものがあります。そのため多数の単体テストをテンプレート内に持っていますが、その利用頻度を考えれば簡単に数百という箇所でインスタンス化され、その中で単体テストの解析をするのでコンパイル時間に大きく影響を与えます。
これを適切にスキップすることで、コンパイル時間やテスト時間、それぞれのメモリ消費をかなり削減できることが確認されました。
いくつか利用頻度の高いライブラリでの計測結果から抜粋すると、以下のような結果が得られているそうです。
dlang/dub:
build 0.00% mem -0.64% time
test -5.44% mem -2.53% time
atilaneves/automem:
build 0.03% mem -2.38% time
test -16.41% mem -13.38% time
rejectedsoftware/diet-ng:
build -0.78% mem -3.08% time
test -23.69% mem -18.77% time
BlackEdder/ggplotd:
build -1.93% mem 1.08% time
test -12.56% mem -10.02% time
eBay/tsv-utils:
build -0.08% mem 0.90% time
test -36.11% mem -22.97% time
libmir/mir-algorithm:
build -0.27% mem -1.55% time
test -1.43% mem -1.69% time
libmir/mir-random:
build -0.12% mem 1.85% time
test -12.47% mem -9.90% time
libmir/mir-core:
build 0.04% mem -7.32% time
test -7.73% mem -8.28% time
なお、この挙動によって何らかのリンクエラーなどの問題が起きたり単体テストが抜け落ちたりするような場合、-allinst
というテンプレートをすべて実体化するためのオプションが回避策として利用できるようです。(実際異常もエラーも起こせず、翻訳含めてこの辺あんまり自信ないです)
コンパイル時間やテスト時間の改善として歓迎しつつ、もしエラーなどに遭遇するようなことがあれば報告していければと思える内容ですね。
強化 : DIP 1034で提案された throw
式が実装されました。
元々 throw
キーワードは「文」として構成されるもので、それは何かの処理として1行占有するような記述しかできませんでした。
今回から「式」として構成されるようになり、色々な場所で書くことができるようになります。
主に用途として挙げられているのが std.sumtype
との連携で書くラムダ式のアロー構文との組み合わせです。
SumType!(int, string) result;
result.match!(
(int num) => writeln("Found ", num),
(string err) { throw new Exception(err); } // ここも (string err) => throw new Exception(err) と書ければ揃うのに () {} という記法しか認められなかった
);
これが、今回から以下のように書けるので見た目がスッキリします。
result.match!(
(int num) => writeln("Found ", num),
(string err) => throw new Exception(err), // 書けるようになる
);
また参考までにですが、このDIP1034というのは throw
式を提案するDIPではなく、「Bottom型」という戻り値が無いことを明示するための型に関する提案です。
Bottom型は最終的に noreturn
という名前になりましたが、いわゆる assert(false)
などで実現していた unreachable
という到達不能性を型として扱うための仕組みです。
DIP1034の実装が進んでBottom型によって戻り値がないことを含め推論できるようになったので、throw
式で終わるラムダ式の戻り値はBottom型、というロジックが成立することからこの変更が可能になりました。
その他様々な言語が持つ類似機能と比較されていて、これだけでもかなりボリュームがあるので参考になるかと思います。
強化 : 初期化子を取得するための __traits(initSymbol)
の追加されました。
初期化子という翻訳は若干不適切かもしれませんが、これを一言で言えば「クラスや構造体の初期化に必要なビットパターン」を静的に得る機能です。
元々 TypeInfo.initializer()
という関数があり、typeid(obj).initializer()
という記述をすれば実行時型情報から得ることができていました。
これがオブジェクトを作らなくても静的に得られるようになった、というのが今回の強化ポイントです。
試しに使ってみると以下のようになります。
class A {
int n = 4;
}
void main()
{
const void[] initSym = __traits(initSymbol, A);
// 対象がクラスであり、ビットパターンの長さはインスタンスサイズと同じです
assert(initSym.length == __traits(classInstanceSize, A));
// ビットパターンにはもちろん初期化に必要な 4 という値が入っています(どこに入ってるかはABIの仕様によるはずなので鵜呑み厳禁)
int n;
(cast(void*)&n)[0 .. 4] = initSym[$ - 4 .. $];
assert(n == 4);
}
ランタイム変更点
強化 : @safe class
の opEquals
についてサポートが追加されました。
言語のオーバーロード解決に対する強化です。
D言語には a == b
といった同値比較をするための演算子をカスタマイズするために opEquals
という関数が用意されています。
クラス定義については継承構造のルートにある Object
型で定義されており、これは @system
として定義されるところからOS機能の呼び出しも認められる「安全でない関数」として扱われていました。
今回の強化により、@safe
で修飾した「安全な opEquals
」というものを定義することにより、それが使われるようになります。
class Test
{
int n;
// こうやって書いておけば @safe にも対応できる
bool opEquals(Test rhs) const @safe
{
return n == rhs.n;
}
}
@safe void main()
{
auto t1 = new Test;
auto t2 = new Test;
assert(t1 == t2);
assert(t1 == t1);
}
ちなみに構造体が標準で提供する同値比較は元から @safe
として扱われています。今回対象となるのはクラスのみです。
改善 : core.sync.rwmutex
は shared
のオーバーロードを持つべきです(そして @safe
コードで使用可能にすべき)。
core.sync.rwmutex
というのは ReadWriteMutex
という排他機構を提供するモジュールです。
ここでは細かく説明しませんが、よくある排他に使う Mutex
の拡張で、利用側の読み書き権限を分けてそれぞれでロックを管理することによりスループットを上げる比較的高度な仕組みを持つ機能です。
ここに shared
という言語の持つスレッド間共有を表すキーワードが関係してくるのですが、今回は shared(ReadWriteMutex)
に関しても適切にロックを取ったりするオーバーロードが使えるようになった、という改善です。
共有されるオブジェクトは shared
で修飾すべきという言語のアイデアと方針はあるものの、実際の機能やライブラリ面で追いつかなかった面がありました。
今回のような改善を積み重ねることで、全面的に shared
を採用したとしても不便を感じない未来を目指している、というのが現状かと思います。
既に Mutex
など多くの既存型は shared
に対応している状況がありますので、前はダメだったよな~という記憶のある方がいれば、是非今の動きを再確認してみてはいかがでしょうか?
ライブラリ変更点
変更 : checkedint
を experimental
の外に移動します。
std.experimental.checkedint
というモジュールが std.checkedint
というモジュールとして正式化されました。
std.experimental
というモジュール階層は元々「実験的」なAPIを入れておくものです。
APIの安定性についてはやや低い扱いで、他にも std.experimental.logger
や std.experimental.allocator
、std.experimental.typecons
があります。
そんな std.experimental
から初めて脱するに至ったモジュールが今回の std.checkedint
です。
今後はAPIや既定値についても原則として変更されず、安定した利用が可能になります。
なお、std.experimental.checkedint
自体はまだ残っており、これは std.checkedint
を public import
することになっています。
これに伴い当然 std.experimental
版は非推奨となりますので、警告を見ることがあればこちらに切り替えてください。
checkedintとは?
簡単に言ってしまえば、「効率的にオーバーフローや精度低下などをチェックする安全な整数型」を提供するモジュールです。
Checked!int
といった形で既存整数型をラップする形で定義できたり、checked(n)
といった関数を通すと変換できるので使うのは簡単です。
試しのサンプルとしては以下のようになります。
void main()
{
import std.checkedint, std.stdio;
writeln((checked(5) + 7).get); // 12
writeln((checked(10) * 1000 * 1000 * 1000).get); // Overflow
}
12
Overflow on binary operator: int(10000000) * const(int)(1000)
結果だけ見れば、演算がオーバーフローすると分かった時点でその演算状態を保存して確認できるようにし、それ以上破滅的な計算を回避する構造になっていることがわかります。
ちなみにこの例は一度破綻したらこれ以上どんな計算をしても同じ結果を返しますが、問題が起きた時点でどんな挙動をするかはテンプレートパラメーターで自由に変更できる仕組みとなってます。
(この巧妙な実装パターンは「Hook」と呼ばれています)
とにかく安全に動作することが求められる産業用途が見据えられていること、それと高度なテンプレート機能が組み合わさった非常に興味深い実装と思います。
他にも様々な機能がありますので、興味があればドキュメントの他のサンプルやソースを見るなどしてみてください。
強化 : std.csv
は、オプションで列数が可変の csv ファイルを扱えるようになります。
なりました!
新しい機能が増えたり使いやすいAPIが増えるだけでなく、こういった品質・柔軟性の改善が継続しているのも良いですね。
個人的にはそんな列が違うCSVを扱いたくないのですが、そういうケースにも対応できるというのは素直に喜べるポイントかなと思います。
改善(変更) : getTempDir()
がプラットフォームごとに異なる動作をする
getTempDir
関数はいわゆる「テンポラリ」と呼ばれる一時領域を指すディレクトリパスを取得する関数です。
この戻り値が環境によって異なっており、「末尾にパス区切り文字が付いてない」ということで必ず末尾に区切り文字が付くようになりました。
POSIXではディレクトリを示すパスとして末尾に区切り文字が付いているほうが健全であるとのことから今回挙動が変更されているようです。
私自身ツール類を書くことが多い身なので、地味に気になる部分として取り上げました。
他に気になる方がいれば念のため利用有無や動作確認をしてみると良いかと思います。
ちなみにですが、元来パス文字列を扱うときは std.path
にある buildPath
関数を使うことが推奨されており、これを使えば区切り文字の有無を意識する必要なくファイル名の結合などが可能です。
日本語Cookbookなどにもパス操作を含む例がありますので、興味があれば一読してみてください。
改善 : SysTime.toISOExtString
はロギングや一貫したファイル名の作成に使用できません。
toISOExtString
は日時に対して 2022-03-11T10:20:30.012345
といった文字列を返す関数です。
一方タイトルがやや分かりづらいですが、要するに「この関数の戻り値においてミリ秒以下の桁が固定でないため、ファイル名に使うと一覧で見たときにガタガタする」という指摘への対応です。(せやな…)
というわけで今回から一貫した精度で結果を出力できるよう、 toISOExtString
の引数に精度のオプションが追加されました。
実際のテストを見ると分かりやすいかと思います。
こういう改善も確かに嬉しいですね。
assert(SysTime(DateTime(-4, 1, 5, 0, 0, 2), hnsecs(520_920)).toISOExtString() ==
"-0004-01-05T00:00:02.052092");
assert(SysTime(DateTime(-4, 1, 5, 0, 0, 2), hnsecs(520_920)).toISOExtString(4) ==
"-0004-01-05T00:00:02.0520");
assert(SysTime(DateTime(-4, 1, 5, 0, 0, 2), hnsecs(520_920)).toISOExtString(2) ==
"-0004-01-05T00:00:02.05");
assert(SysTime(DateTime(-4, 1, 5, 0, 0, 2), hnsecs(520_920)).toISOExtString(7) ==
"-0004-01-05T00:00:02.0520920");
ちなみにこの関数は標準ライブラリの中では日本人にもなじみのある年月日形式に一番近い形式でフォーマットしてくれる関数です。雑に date.toString()
とすると月名がローマ字になったりしますので、この関数はぜひ覚えておくと良いかと思います。
改善 : totalCPUs
がより正確な CPU 数を返すようにします。
totalCPUs
は std.parallelism
に定義されたCPU数を得る関数ですが、これがLinux環境でより正確なCPU数を返すようになります。
どういうケースで不適切な値が得られていたのかまでは分かりませんでしたが、若干挙動が変化する可能性については認識しておくと良いかと思い取り上げました。
一部Rubyのソースコードを参考にされているようでしたので信頼できる結果が得られるかと思います。
非同期処理が当たり前になる昨今、こういう基礎的な関数は重要ですからね。
DUB変更点
変更 : 実行ファイルやDLLと共にPDBファイルを targetPath
にコピーするようにします。
PDBファイルは、Windows環境でデバッグに使うシンボル情報を持ったファイルです。
これが実行ファイルと同じ targetPath
で指定されたフォルダにコピーされるようになります。
Windows環境ではデバッグなどに便利なので歓迎される変更なのですが、debug
でも release
でも関係なく生成されるようになりますので要注意です。
たとえば dub build
の結果を丸ごとZIPにして配るようなケースでは pdb ファイルが同梱されるようになる可能性があります。
この場合、もし dub.sdl
で publish
などの設定を書いてリリースパッケージを構成する場合、以下のような記述によって *pdb
をすべて削除できますので参考としてください。
config "publish" {
postBuildCommands "del %DUB_TARGET_PATH%\\*.pdb /q" platform="windows"
}
インストーラー変更点
改善 : dmdを LTO
と PGO
を有効にしてビルドしたものに置き換えます。
ただでさえ速いD言語コンパイラがまだ速くなるかもしれません。
LTO
は Link Time Optimization の略で「リンク時最適化」、PGO
は Profile-guided Optimization の略で「プロファイル誘導最適化」などと呼ばれ、いずれもプログラムの実行時間短縮に寄与する最適化技術です。
dmd 自体が、高度な最適化機能を持つコンパイラ基盤であるLLVMベースのD言語コンパイラである ldc によってビルドされて配布されるため、LLVMの持つ LTO
や PGO
といった機能を活用することができます。そしてそれらが今回から有効になりました!
実際 dmd がどの程度速くなったかは情報見つかりませんでしたが、こちら元々国際的な決済プラットフォームを提供する eBay が作っている TSVUtils というツールで実証された効果を参考にしているようです。
一文引用させていただくと、タスクによっては25%以上の短縮ということで、バッチ処理のような重いプログラムにはかなり有効だろうと思われる結果です。
LTOとPGOの組み合わせにより、標準的な6つのベンチマークのうち3つで25%以上、6つのうち5つで8%以上の性能向上が見られました。
改善 : install.sh: ARM64 をアーキテクチャとして認識するようになります。
何のことかと思って中身を調べると、M1 Macのアーキテクチャが ARM64 で、install.sh
がそれを認識しなかったため追加した、というものです。
元からM1 Macにバイナリ持っていって動くのは確認されているのですが、インストールスクリプトは自動でDL対象を切り替えたりする機能を持っているので、そこにちょっとサポート不足があったということでした。
M1 Macを使っている方も安心してインストールできるようになりますので、ぜひインストールスクリプトを使ってみてください。
インストールスクリプトの使い方や雰囲気は以下の記事が参考になるかと思います。
dlang.org 変更点
改善 : OpenBSD をサードパーティのダウンロードリストに追加します。
トピックとして詳細取り上げていませんが、今回はOpenBSDサポート回と言っても良いくらい多数の強化が行われたバージョンアップです。
昨年11月に行われたDConf Online 2021でもOpenBSD対応の経験を話すセッションが設けられていましたが、プログラミング言語としても環境サポートの幅広さとても重要になってきます。
元々公式提供でないサードパーティ環境として以下のようなラインナップがありましたが、今回から OpenBSD の doas
というパッケージマネージャーからも dmd
, dub
, dtools
が追加できるようになっています。
今後も更に広まっていくことを期待しています。
環境 | パッケージマネージャー等 | パッケージ名 |
---|---|---|
Arch Linux | pacman | dlang |
Gentoo | layman | dlang |
Homebrew | brew | dmd |
Nix/NixOS | nix-env | nixpkgs.dmd |
Ubuntu/Debian | apt-get |
dmd-compiler , dub
|
Docker | docker | dlang2/dmd-ubuntu |
OpenSUSE Tumblewee | zypper | dmd |
Snap | snap |
dmd , dub , ldc2
|
OpenBSD (追加) | pkg_add |
dmd , dub , dtools
|
非推奨または廃止される機能
実験的機能に関する非推奨が1件とエラーが1件です。
そこまで対処の難しいものはありませんでしたので、エラーに遭遇したらメッセージ通りの対処でも割と何とかなるかなと思います。
個々の対応方法についてもまとめていきます。
非推奨
-
std.experimental.checkedint
というモジュールが非推奨になりました。
削除/エラー
-
switch case
のフォールスルーはエラーになるように変更されました。
非推奨 : std.experimental.checkedint
モジュール
トピックのほうでも取り上げていますが、std.checkedint
というモジュールに移りました。
実験的機能を脱したということで今後は安定して利用できますので、今後はそちらを利用いただけばOKです。
なお発生するエラーは以下のようなものです。見かけたら .experimental
を取ればOKです。
app.d(7): Deprecation: module `std.experimental.checkedint` is deprecated
エラー : switch case
のフォールスルー
2016年10月31日の dmd 2.072.2 の頃(実際はもっと前?)から非推奨で警告が出ていたものが今回エラーになりました。
内容は switch case
構文におけるフォールスルーという動作です。
C言語でもよく問題視されるものですが、D言語は互換性を維持しつつもエラーにしたいとの声が根強い動作でした。
例を挙げると以下のような感じです。
void parseNumFmt(char c, out int base, out bool uppercase)
{
switch (c)
{
case 'B': // case内にコードがないので、これは許可されたフォールスルー
case 'b':
base = 2;
// 処理があるのに次のcaseに移る処理があり、これがフォールスルーのためエラー
case 'X':
uppercase = true;
goto case; // 明示的なフォールスルーは goto case; と記述すればOK
case 'x':
base = 16;
break;
default:
break;
}
}
これに対し、今までは以下のような警告が出ていました。switch case fallthrough
とあり、goto case;
を使うように指示されます。
onlineapp.d(11): Deprecation: switch case fallthrough - use 'goto case;' if intended
2098
なおエラー自体はフォールスルー先である「次の case
」に対して発生しますので、そのすぐ上に goto case;
を書けば良い、と言う感じです。
まとめ
昨年行われた国際カンファレンスの DConf Online 2021 が行われてから初めてのリリースとなりました。
OpenBSDサポートによって一層幅広く利用できるようになった一方で、既存ユーザーもありがたいコンパイル時間の改善や強化が複数盛り込まれたリリースとなっています。
全件深掘りすることはできませんでしたが、使い勝手の改善など身近な部分は多く取り上げることができたと思います。
ぜひぜひ参考にバージョンアップ等検討ください。
さて、リリースは4ヶ月サイクルで年3回に落ち着きつつあるような気もしますが、次回は7月ごろでしょうか?
国際情勢も気になるところですが、夏にはイギリスで行われるオフライン版の DConf 2022 が計画されていますので、そこに向けて突き進んでいく第2四半期ということになろうかと思います。
裏ではまた別の強化改善について議論が進められていますので、引き続き期待して待ちたいと思います!