要約
このレッスンは、テキストとファイルを扱うコースの第七単元を締めくくる。黙ってデータを失い、ASCII 以外の文字を含む名前を正しく測れなかった導入のプログラムを、学んだことで書き直す。文字列からファイルの形式へと進む概念の連鎖を組み立て直す。そのうちどの言語でも通用することと C++ に固有のことを分ける。単元の未定義動作を、サニタイザが止めるものと誰にも見えないものとともにまとめる。まとめテストで導入テストの技能を繰り返す。そして単元全体を組み合わせた六つの挑戦課題で終わる。
学習目標
- 実際のテキストをそのエンコーディングとともに処理する。文字列のバイト数を予測し、その文字を数え、UTF-8 を壊す操作を避ける。
- 入力とファイルを堅牢に読み、不正なデータとデータの終わりを区別し、各データを完全なテキストとして検証する。
- 指定された形式で結果のファイルを作り、不正な行をそれぞれ行番号とともに報告する。
- 単元の未定義動作(ダングリングビュー、文字列の外の添字)を見分け、同じく黙って起こる定義された失敗と区別する。
- 単元で学んだことのうち、どの言語でも通用することと C++ に固有のことを分ける。
単元で達成したこと
この単元は、テキストをデータとして扱うことを教え、不正な形式の入力に出会っても壊れずにファイルを読み書きするプログラムを書けるようにすることを目指した。その目的は、導入が既知とした事柄だけに基づいて達成された。第 2 単元の「コードとしての文字」、第 3 単元のビット、第 6 単元のベクターとレコード、そして第 5 単元の契約つきの関数である。>> で読んでいたために佐藤 花子と翔太を失い、「健太」を「Ken」より長いと判定していた成績一覧のプログラムは、今では次のように書ける。ファイルの行を読み、最後の空白で点数を切り分け、それを関数 parseInt(レッスン 入力を正しく読む のもの)で検証し、不正な行をエラーストリームで報告し、名前を文字数で測り、一覧を別のファイルに残す。例を自己完結させるために、プログラムはまずデータのファイルを書く。実際には、このファイルはすでに存在しているはずである。
#include <charconv>#include <cstddef>#include <fstream>#include <iostream>#include <print>#include <string>#include <string_view>// テキストがちょうど一つの整数であるかを判定する。// 事前条件:なし。// 事後条件:レッスン「入力を正しく読む」のものと同じ。bool parseInt(std::string_view text, int& value){ int result{}; const auto last{text.data() + text.size()}; const auto [stop, error]{std::from_chars(text.data(), last, result)}; if (error != std::errc{} || stop != last) { return false; } value = result; return true;}// UTF-8 のテキストの文字数。// 事前条件:text は正しい UTF-8 である。// 事後条件:text の文字数(バイト数ではない)を返す。std::size_t letterCount(std::string_view text){ std::size_t count{0}; for (const char c : text) { if ((c & 0xC0) != 0x80) { ++count; } } return count;}int main(){ { std::ofstream out{"grades.txt"}; std::println(out, "Ken 65\n健太 70\n佐藤 花子 58\n翔太 六\n陽菜 61"); } std::ifstream in{"grades.txt"}; std::ofstream report{"report.txt"}; if (!in || !report) { std::println(std::cerr, "ファイルを開けませんでした"); return 1; } std::string line{}; std::string longest{}; int lineNumber{0}; while (std::getline(in, line)) { ++lineNumber; const std::size_t space{line.rfind(' ')}; int grade{}; if (space == std::string::npos || !parseInt(std::string_view{line}.substr(space + 1), grade)) { std::println(std::cerr, "{} 行目が不正:「{}」", lineNumber, line); continue; } const std::string name{line.substr(0, space)}; std::println(report, "{:<10}{:>3}", name, grade); if (letterCount(name) > letterCount(longest)) { longest = name; } } std::println("最も長い名前:{}({} 文字)", longest, letterCount(longest));}4 行目が不正:「翔太 六」最も長い名前:佐藤 花子(5 文字)画面に表示されるのはエラーメッセージと結論だけである。一覧はファイルにあり、プログラムを実行したあとでコマンド cat(PowerShell では Get-Content)で表示できる。
$ cat report.txtKen 65健太 70佐藤 花子 58陽菜 61それぞれの部分が導入の欠陥の一つに応えている。行ごとの読み取りと rfind によって空白を含む名前が扱える。npos と parseInt は翔太の行を退け、それを行番号とともに報告し、陽菜の行へと進ませる。letterCount は文字を数えるので、五文字で十三バイトの「佐藤 花子」が正当に勝つ。そして report.txt は、main が終わってそのストリームが閉じたときに完全になる。
文字列
レッスン 文字列 は、std::string を、テキストの操作をもつ char の並びとして示した。find による検索(位置か npos を返す)、その値をつねに明示的に比較すること、substr による抽出(有効な位置を要求する)を教え、引数としては安全だが参照するテキストより長く生き残ると危険な std::string_view と、対比としての C 文字列を示した。
テキストは ASCII ではない
レッスン テキストは ASCII ではない は、文字とそのコードポイントとそのバイトを区別し、UTF-8 がビットを一から四バイトにどう配分するかを説明した。そこから、size がバイトを数えること、文字は (c & 0xC0) != 0x80 で数えること、バイトごとの反転、大文字化、切り詰めはテキストを壊すが、検索と連結は壊さないこと、そして標準ライブラリが何をしないかを導いた。
入力を正しく読む
レッスン 入力を正しく読む は、入力を信頼できない境界として扱った。std::getline で行を読むこと、fail() と eof() で不正なデータ、収まらない数、データの終わりを区別すること、clear と ignore で回復すること、そして std::from_chars で数をテキストとして検証し、テキスト全体が消費されることを求めることを教え、最後に、契約をもつ境界の関数である parseInt と readInt で締めくくった。
テキストファイル
レッスン テキストファイル は、ファイルがストリームであり、std::cin と同じ状態をもつことを示した。開いたことを確かめるのを規則とし、while (!in.eof()) がなぜデータを一つ余計に処理するのかを証明し、std::println(out, …) で書くこと(置き換えるか追記するか)と、std::cerr でメッセージと結果を分けることを教え、すべてを指定され検証された行の形式へとまとめた。
コンテンツを読み込んでいます…