std.stdioとは?D言語の標準入出力とwriteln・readln・Fileの使い方

stdio という名前は C 言語の stdio.h に由来し、D 言語では標準ライブラリ Phobos の std.stdio がその役割を引き受けます。ただし中身は C の関数群をそのまま並べたものではありません。File 型が FILE* を参照カウントで保持し、最後の参照がスコープを抜けた時点で自動的に閉じます。この記事は DMD 2.112.0 時点の公式ドキュメントと Phobos のソースを基準に、出力4関数の使い分け、readln と readf でつまずく箇所、File のオープンモードと byLine の罠、バイナリ入出力の戻り値までを整理したものです。

まとめ|std.stdioで最初に押さえる5点

  • まず writeln と readln の2つで動きますwriteln(args)write(args, '\n') と等価で、書式が要るときだけ writef 系に切り替えれば足ります。
  • readln は EOF で null を返し、戻り値には改行文字が含まれます。判定は !is null、行末を落とすなら strip を通します。
  • ここから先は詰まったときに戻る箇所です。数値の読み取りは readf!" %d"(x) の先頭スペースが必須で、1行に複数個並ぶなら readfln のほうが素直に書けます。
  • File は close() を書かなくても閉じます。参照カウントで管理されるため、C 言語のように fclose を呼び忘れる事故は起きません。
  • 行を保持するなら byLine ではなく byLineCopy。byLine の要素は次の行を読んだ時点で内容が壊れます。

以降では、この5点それぞれの根拠と、公式ドキュメントに書かれているのに見落とされやすい挙動を順に確認していきます。

std.stdioの位置づけ|C言語のstdio.hとの対応とimportの書き方

stdio.hとの対応関係|FILE*を参照カウントで包むFile型

D 言語は C の標準ライブラリをそのまま呼び出せます。core.stdc.stdio を import すれば printffopen も使えます。それでも std.stdio が別途用意されている理由は、C の FILE* が生ポインタであり、閉じ忘れと二重解放を招くためです。解放済みポインタ経由のクラッシュはセグメンテーションフォルトとして表面化します。

std.stdio の File は、この FILE* を参照カウント付きで保持する構造体です。公式ドキュメントは「ある FILE* に束縛された最後の File 変数がスコープを抜けた時点で、その FILE* は自動的に閉じられる」と明記しています。D はガベージコレクションを備えた言語ですが、ファイルハンドルの解放は GC の回収タイミングに委ねず、参照カウントで決定的に処理する設計です。

import宣言|選択的インポートでシンボル衝突を避ける

モジュール全体を取り込む import std.stdio; は最短ですが、std.file と併用すると write が衝突します。std.stdio の write は標準出力への書き出し、std.file の write はファイルへのバイト列書き込みで、名前だけが同じです。

import std.stdio : writeln, File;
import std.file  : readText, write;

void main()
{
    write("out.txt", "内容");   // std.file.write が呼ばれる
    writeln(readText("out.txt"));
}

必要なシンボルだけを列挙する選択的インポートにすれば、どちらの write を呼んでいるかがコード上で確定します。衝突が起きてから std.stdio.write(...) と完全修飾で書き直すより、最初から列挙しておくほうが読み手の負担は小さくなります。

標準出力の4関数|書式指定と改行の2軸で選ぶ

4関数の使い分け|write・writeln・writef・writeflnの選択基準

関数 書式指定 末尾の改行 使いどころ
write なし なし 途中経過を1行に連結
writeln なし あり 通常の結果表示・ログ
writef あり なし 区切り文字を自分で置く
writefln あり あり 桁揃えした1行の出力

4つの違いは「書式指定の有無」と「改行の有無」の2軸だけです。公式ドキュメントは writeln(args)write(args, '\n') と等価と定義しており、両者に性能差はありません。引数は可変個で、型ごとの指定子を書かなくてもそのまま連結されます。

import std.stdio;

void main()
{
    writeln("Hello, World!");
    write("合計: ");
    writeln(1 + 2, " 件");        // 数値と文字列を混ぜて渡せる
    writefln("%-8s|%5.2f", "rate", 0.125);
}

実務では writeln を既定にして、桁を揃える必要が出たところだけ writefln に替えるのが素直なやり方です。すべてを writefln で書くと、書式文字列と引数の対応を読む手間が増えるだけで得るものがありません。

writefln!”…”のコンパイル時フォーマット検査|書式ミスをビルドで止める

writef と writefln には、書式文字列をテンプレート引数として渡す形があります。この形は内部で std.format.checkFormatException を呼び、結果を static assert にかける仕組みです。つまり書式と引数型の食い違いがコンパイルエラーになります。

import std.stdio;

void main()
{
    writefln!"%s は %d 点"("合計", 87);    // 型が合わないとコンパイル時に停止
    writefln("%s は %d 点", "合計", 87);   // 誤りは実行時まで発覚しない
}

書式が実行時に決まる場合を除き、テンプレート版を選ばない理由はありません。C の printf で「%d に文字列を渡して落ちる」種類の事故が、ビルドの時点で止まります。

標準入力|readlnのEOF判定とreadf・readflnの書式指定

readlnの戻り値|終端文字込み・EOFでnull

readln は標準入力から1行を読み、行終端文字を含めた文字列を返します。ファイル終端に達すると null を返すため、判定は !is null で行います。公式ドキュメントが載せている標準入力の丸写しコードが、そのまま定型です。

import std.stdio;

void main()
{
    string line;
    while ((line = readln()) !is null)
        write(line);              // line に改行が含まれるので write でよい
}

ここで writeln を使うと改行が二重になります。行末を落としたい場合は std.string.stripchomp を通してください。引数なしの readln は呼び出しごとに新しい文字列を確保します。数十万行を読む処理では、バッファを渡す readln(buf) 形にすると読み取りバッファを再利用でき、確保回数を減らせます(長い行が来ればバッファは必要に応じて拡張されます)。

readfで数値を読む|先頭スペース1個で挙動が変わる

数値を直接読み取る readf は、書式文字列の先頭に空白を1つ置くかどうかで結果が変わります。公式ドキュメントの例も readf!" %d"(a) と先頭に空白を置いており、この空白が「直前に残っている空白や改行を読み飛ばす」指示になります。

import std.stdio;

void main()
{
    foreach (_; 0 .. 3)
    {
        int a;
        readf!" %d"(a);           // 先頭の空白がないと2回目以降が失敗する
        writeln("読み取り: ", a);
    }
}

先頭の空白を省くと、直前の読み取りで残った空白や改行を数値として読もうとして失敗します。「最初の1個だけ読めて2個目から動かない」という症状の大半はこれです。なお readf の戻り値は「埋められた変数の個数」なので、入力が途中で尽きたかどうかはこの数で判定できます。readf のテンプレート版も writefln と同じくコンパイル時に書式を検査します。

readflnと分割変換|1行に複数の値が並ぶ入力の読み方

1行に空白区切りで複数の値が並ぶ入力なら、readfln が正攻法です。1行を読んでから formattedRead で解析する関数で、行の切れ目を跨がないぶん先頭スペースの調整が要りません。戻り値は埋まった変数の個数なので、これをループ条件にできます。

import std.stdio;

void main()
{
    int a, b, c;
    while (readfln("%d %d %d", a, b, c) == 3)
        writeln(a + b + c);
}

列数が入力ごとに変わるなら、readln で1行取ってから分割するほうが柔軟です。readln().strip().split() で文字列の配列にし、map!(to!int) で数値へ変換します。入力形式が崩れる可能性があるなら、readf で直接読むよりこの形のほうが ConvException を捕まえる位置を明確にできます。

ファイル入出力|File型のオープンモードと自動クローズ

File型の自動クローズ|参照カウントでclose()を書かない設計

File のコンストラクタは第2引数にオープンモードを取り、既定値は "rb" です。C の fopen と同じ文字列がそのまま通るので、書き込みは "w"、末尾への追記は "a"、バイナリなら "rb""wb" を渡します。開けなかった場合は ErrnoException が投げられます。

import std.stdio;

void main()
{
    auto f = File("report.txt", "w");
    f.writeln("1行目");
    f.writef("%s=%d", "count", 3);
    f.writeln();
    // f.close() は不要。f がスコープを抜けた時点で閉じられる
}

ただし「常に close を書かなくてよい」わけではありません。File のデストラクタは detach を呼び、公式ドキュメントはここで「閉じる際に失敗すると ErrnoException を投げる」と定めています。ディスクフルなどで最終フラッシュが失敗した場合、デストラクタから例外が飛ぶことになります。書き込みが成功したかどうかを自分で判定したいなら、flush()close() を明示して、その場で例外を受けてください。閉じる時点を制御したい場合も同じです。

byLineとbyLineCopy|行を保持するなら後者

ファイルを行単位で読む入力レンジが byLine です。公式ドキュメントは「各 front は popFront の呼び出し後には維持されないため、保持が必要なら呼び出し側が内容を複製しなければならない」と注記しています。バッファを使い回して確保を減らす設計のため、読み終えた行のスライスは次の行で上書きされます。

import std.array, std.stdio;

void main()
{
    // NG: 全要素が最後に読んだ行の残骸を指す
    auto bad = File("in.txt").byLine().array;

    // OK: 行ごとに複製されるので配列に貯めても壊れない
    auto ok = File("in.txt").byLineCopy().array;
}

その場で集計して捨てるなら byLine、配列やハッシュに貯めるなら byLineCopy、と読み方で決めます。自前で byLine.map!idup と書く手もありますが、byLineCopy は front をキャッシュするぶんこれより確保が少なくて済むと公式ドキュメントが注記しており、手書きする理由はありません。KeepTerminator.yes を渡せば行終端を残せるので、元ファイルをそのまま再構成したいときに使ってください。行番号が要る場合の lines は File のメンバではなく std.stdio 直下の struct で、公式ドキュメントも参照先として byLine を挙げています。新規に書くなら byLine 系で統一するのが無難です。

バイナリ入出力|rawRead・rawWriteの戻り値とバイトオーダーの決め方

rawRead は fread を、rawWrite は fwrite をそのまま呼びます。読み書きする要素数と要素サイズは、渡した配列の長さと型から推論されます。Windows でも常にバイナリモードで処理されるため、改行変換は入りません。失敗時に投げられるのは StdioException ではなく ErrnoException です。

間違えやすいのは rawRead の戻り値です。公式ドキュメントは「実際に読み込まれたデータを含む buffer のスライス」と定義しており、EOF に達すると渡した配列より短くなります。buf.length をそのまま使うと、末尾に前回の残骸が混ざります。

import std.stdio;

void main()
{
    auto f = File("data.bin", "rb");
    auto buf = new ubyte[4096];

    ubyte[] got = f.rawRead(buf);   // 戻り値を使う。buf を使わない
    writeln(got.length, " バイト読み込み");

    File("out.bin", "wb").rawWrite(got);
}

ファイル全体が大きくメモリに載せられないなら、byChunk で固定長ずつ受け取る形にすると、確保するバッファを一定に保てます。

バイトオーダーの扱いは自分で決めてください。構造体をそのまま rawWrite すると、パディングと整列規則がバイト列に現れます。メモリアライメントが異なる環境で読み戻す前提なら、フィールドを固定幅整数で並べ直したうえで、std.bitmanipnativeToLittleEndianlittleEndianToNative(ビッグエンディアン版は nativeToBigEndianbigEndianToNative)を通してバイト列を確定させます。外部と交換するファイル形式を構造体の生バイトで済ませるのは、移植性を捨てる判断です。

標準ストリーム|stdin・stdout・stderrの実体とリダイレクト時のバッファリング

3本のストリームの実体|File型を返すスレッドグローバル

D の stdinstdoutstderr は特別な構文ではなく、C の対応するストリームを包んだ File 型を返すスレッドグローバルです。File のメンバがそのまま使えるので、stderr.writeln(...)stdin.byLine() と書けます。

3本のテキストストリームが起動時に用意されること自体は ISO C が定めています。それらがファイルディスクリプタ 0・1・2 に対応すると定めているのはPOSIXのほうで、シェルの 2> でエラーだけを別ファイルへ振り分けられるのはこの番号付けがあるからです。Windows は POSIX 準拠ではありませんが、C ランタイムが同等の番号を提供しています。エラーや警告を stderr に出しておけば、標準出力をパイプで次のコマンドへ渡しても診断メッセージが混ざりません。

リダイレクト時のバッファリング|Windowsには行バッファが無い

std.stdio は C の FILE* をそのまま使うため、バッファリングの規則も C 標準ライブラリに従います。ISO C が定めているのは「標準エラーは全バッファにしない」「標準入力と標準出力は、対話デバイスを参照しないと判定できる場合に限り全バッファ」の2点だけで、行バッファは規定されていません。端末に繋がっているときの行バッファは glibc をはじめとする多くの Unix 系実装の挙動であって、規格上の保証ではありません。

この差が Windows で表に出ます。Microsoft の setvbuf ドキュメントは _IOLBF について「一部のシステムでは行バッファを提供するが、Win32 では _IOFBF(全バッファ)と同じ挙動になる」と明記しており、Win32 の C ランタイムには行バッファが存在しません。改行を書けば押し出されるという前提は、Windows では成り立たないわけです。

import std.stdio;

void main()
{
    foreach (i; 0 .. 3)
    {
        stdout.writeln("進捗 ", i + 1, " / 3");
        stdout.flush();           // 環境に依存せず即座に反映される
    }
    stderr.writeln("警告: 入力が空です");
}

進捗表示が「端末では出るのにログファイルには最後まで何も出ない」現象は、リダイレクトで全バッファに切り替わったことが原因です。長時間動くバッチで途中経過を残すなら、書いた直後に flush を呼ぶか、最初から stderr に出す設計にしてください。stderr は ISO C が全バッファにしないと定めているため行単位で滞留することはありませんが、改行を含まない途中出力までが即座に届く保証はありません。出力先が機械処理されるなら、行ごとの自由文よりも構造化ログの形式に寄せたほうが後段の集計が楽になります。

エラー処理|StdioExceptionとErrnoExceptionの捕まえ方

std.stdio が投げる例外は、主に StdioExceptionErrnoException の2つです。境界は「読み書きの種類」ではなく「C 側のどの関数が失敗したか」で決まります。open・close・flush と、fread / fwrite を直接呼ぶ rawRead・rawWrite の失敗が ErrnoException、write / writeln / readln / byLine といったテキスト系の入出力エラーは StdioException です。この2つは継承関係にないため、片方だけを catch すると他方は素通りします。

import std.exception : ErrnoException;
import std.stdio;

void main()
{
    try
    {
        foreach (line; File("missing.txt").byLineCopy())
            writeln(line);
    }
    catch (ErrnoException e)
        stderr.writeln("開けません: ", e.msg);
    catch (StdioException e)
        stderr.writeln("読み取りに失敗: ", e.msg);
}

この2つ以外にも、readln は Unicode 変換に失敗すると UnicodeException を、readf は書式どおりに変換できないと ConvException を投げます。文字コードが混在する外部ファイルや、人手で編集された入力を読むコードでは、この2つも想定しておいてください。なお catch した内容を writeln で標準出力に流すと、正常な出力とエラーが同じストリームに混ざります。診断は stderr に固定するのが原則です。

Windowsでの文字化けと改行|コードページとテキストモードの落とし穴

std.stdio には、Windows コンソール向けに WriteConsoleW を直接呼ぶ経路がありません。書き出しは C ランタイムの FILE* を通るため、コンソールに表示される日本語は現在のコードページに左右されます。ソースを UTF-8 で保存していても、コンソールのコードページが 932 のままだと writeln の出力は化けます。

対処は2通りあります。手軽なのはコンソール側を UTF-8 に切り替える方法で、コマンドプロンプトなら chcp 65001 を実行してからプログラムを起動してください。もう1つは _setmode で標準出力を _O_U8TEXT などのワイド指向に設定する方法で、Phobos の Microsoft ランタイム向け分岐がこのモードを検出し、ワイド文字の出力経路(fputwc)へ切り替えます。出力先がファイルやパイプであれば化けないため、「実行画面だけ崩れる」場合はまずコードページを疑ってください。

改行にも環境差が出ます。テキストモード("w" など)で開いたファイルへ writeln すると、Windows では CRLF に変換されて書き込まれます。Phobos 自身の単体テストが、Windows とそれ以外で期待値を "\r\n""\n" に分けているほどです。バイト数を数える処理や、行末を含めて比較する処理では、この差が結果を変えます。改行変換を避けたいなら、バイナリモード("wb")で開くか rawWrite を使ってください。

std.stdioを使わない判断|std.fileで一括読み込みすべき場面

設定ファイルや小さな JSON を読むだけなら、File と byLine を持ち出す必要はありません。std.file.readText が全文を1行で返します。

import std.file : readText;
import std.stdio : writeln;

void main()
{
    string all = readText("config.json");
    writeln(all.length, " 文字");
}

判断の分かれ目はファイルサイズと処理単位です。全体をメモリに載せて構わないサイズなら readText、載せられないか行単位で逐次処理したいなら File と byLine、と決めておけば迷いません。数 MB 程度の設定ファイルや辞書に対して byLine のループを書き、行を配列へ貯め直しているコードは、readText 1行に置き換えられます。

数 GB のログを集計する処理で readText を使うのは避けてください。全文をメモリに確保するため、GC の負荷とピークメモリが一気に跳ね上がります。この規模では byLine で1行ずつ処理し、結果だけを保持する形にします。「とりあえず std.stdio を import して File で開く」という反射は、小さいファイルに対しては単に冗長です。

よくある質問

stdioとは何の略ですか?

standard input/output(標準入出力)の略です。読み方は「エスティーディーアイオー」「スタンダードアイオー」のいずれも使われ、統一された呼称はありません。C 言語の標準ライブラリで入出力関数を宣言するヘッダ stdio.h の名前として広く知られており、多くの言語がこの呼び名を引き継いでいます。D 言語では Phobos のモジュール std.stdio がこの役割を担い、コンソールへの出力、キーボードやパイプからの入力、ファイルの読み書きをまとめて提供するモジュールです。名前は共通でも、D 側は File 型による自動クローズなど独自の設計を持ちます。

D言語でprintfは使えますか?

使えます。import core.stdc.stdio; と書けば C の printf をそのまま呼び出せます。ただし printf は書式指定子と引数の型が合っているかをコンパイラが検査しないため、誤りが実行時のクラッシュや意味不明な出力として現れる点に注意してください。D では writefln!"%d"(x) の形が書式をコンパイル時に検査するので、C 資産との相互運用で必要な場面を除き、printf を選ぶ理由はありません。可変個引数をそのまま渡せる writeln も同じ用途を満たします。

writeとwritelnの違いは何ですか?

末尾に改行を付けるかどうかだけが違います。公式ドキュメントは writeln(args)write(args, '\n') と等価であると定義しました。writeln は引数なしでも呼べるため、空行を1行入れたいときは writeln(); と書けます。書式を指定する場合の writef と writefln も、同じ関係にあります。

readlnで読んだ文字列を数値にするには?

readln の戻り値には行終端文字が含まれるので、そのまま変換すると失敗します。std.string.strip で前後の空白と改行を落としてから std.conv.to!int に渡してください。to!int(readln().strip()) の形が定型です。数値だけが並ぶ入力なら、readln を経由せず readf!" %d"(x) で直接読む手もあります。

File型はclose()を呼ばなくていいですか?

通常は不要です。File は FILE* を参照カウントで保持し、その FILE* に束縛された最後の File 変数がスコープを抜けた時点で自動的に閉じられます。C 言語の fclose 呼び忘れに相当する事故は起きません。明示的な close が要るのは、閉じる時点を自分で制御したい場合と、閉じる処理の失敗を検出したい場合です。デストラクタ経由のクローズが失敗すると ErrnoException がそこから投げられるため、書き込み結果の成否を確認したいなら flush()close() を自分で呼んでください。

関連記事

資料請求

RELATED POSTS 関連記事