ファイルとエラー | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、前のページでは省略した、ファイルを使ったプログラムの中で起こるエラーについて取り上げます。エラーが起きているかどうかをどうやって調べるか、エラーが起きていたらどうすればいいか確認します。また、エラーが起きたことを伝えるメッセージの出力方法や、プログラムの実行を止める方法についても取り上げます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



エラー

前のページで、ファイルを使ったプログラムを取り上げましたが、話を簡潔にするため、エラーが起こる可能性を無視しました。このページでは、エラーへの対応について説明していきます。

エラーが発生したときにまずすべきことは、エラーが発生したという事実を示すことです。「うまく動作しているように見えるが、実はエラーが起きていて、正しい処理が行われていなかった」ということは避けなければなりません。そこでエラーメッセージを出力するようにします。これはいつものように std::cout を使って出力することもできますが、このページでは新しい方法を紹介します

エラーメッセージを出せば終わりということではありません。エラーが起きている以上、その続きの処理は正常に動作しないはずですから、何かしらの対処が必要です。その方法は大きく分けて2つあります。1つは、プログラムの実行を速やかに中止すること。もう1つは、ほかの処理に切り替える手段を用意することです。

たとえば、ファイルのオープン時にエラーが起きた場合を考えてみます。そのプログラムが、必ずファイルの中身に対して処理をしなければならないのであれば、ファイルをオープンできないことは致命的であるといえるでしょう。こういう場合はよけいなことをせず、速やかにプログラムの実行を止めます。プログラムの実行を止める方法については、あとで取り上げます

一方、「ファイルから読み込めなければ、デフォルトのデータを使う」といったことが可能なのであれば、プログラムの実行を止めなくていいということになります。

標準エラー

エラーメッセージを出力するとき、これまでどおり std::cout を使ってもいいのですが、もう1つ、std::cerr を使う方法があります。std::cout が標準出力へ出力するのに対し、std::cerr は標準エラー (standard error) と呼ばれるストリームへ出力します。使い方は同じと考えていいです。

#include <iostream>

int main()
{
    std::cout << "Hello.\n";
    std::cerr << "Hello.\n";
}

実行結果:

Hello.
Hello.

標準出力と標準エラーの具体的な出力先は、ほとんどすべての環境で「画面」です。そのため、どちらを使っても結果は変わらないようですが、プログラムの正常な進行の中で出力するメッセージは std::cout を使って標準出力へ出力し、エラーメッセージは std::cerr を使って標準エラーへ出力するという使い分けをすることが多いです。その理由は2つあります。

1つには、std::cerr は、ただちに出力される保証があることです。そういうと std::cout には保証がないようですが、実際、std::cout による出力は、すぐに出力される保証はありません。これにはバッファリング (buffering) という仕組みが関係しています。バッファリングについては後ほど取り上げます。

もう1つには、出力先は変更できるからです。(そのようにソースコードを書けば)プログラマーの意思で変更できますし、プログラムを実行するユーザーの側でもリダイレクト (redirect) という機能を使って変更できます。そのため、std::cout と std::cerr を使い分けるようにプログラムを作っておけば、エラーメッセージだけを別の場所に書き出して取っておくとか、正常時のメッセージが大量にあるときに、重大なエラーメッセージが埋もれないようにするといったことが可能になります。

Windows のコマンドプロンプトでリダイレクトをおこなう方法について、Windows編>コマンドプロンプト>リダイレクトのページで説明しています。

【上級】プログラムからストリームの接続先を変更するには、C言語から引き継いだ std::freopen関数(C言語編のリファレンス)を使います。std::cin は stdin に、std::cout は stdout に関連付いているため1、std::freopen関数で stdin や stdout の接続先を変更すると、std::cin や std::cout も影響を受けます。

バッファリング

バッファリングとは、出力しようとするデータをすぐには出力せずに、いったんバッファ (buffer)と呼ばれる場所に蓄えておいて、溜まってきてから一気に出力するという仕組みのことです。

実際にどこかに出力するという行為は、それを1回おこなうたびに多くの処理時間を必要とします。そのため、ある程度データが溜まってきてから、1回にまとめて出力したほうが効率的といえます。

バッファに溜まっているデータを本来の出力先へと送り出すためには、フラッシュ (flush)という操作を行います。フラッシュは適切なタイミングで自動的に行われるほか、プログラムの指示で強制的に行うこともできます

バッファリングの仕組みが使われていると、出力したつもりでいたデータが、実はまだバッファに残されたままだったということが起こり得ます。普段は意識しないにしても、バッファリングという仕組みがあることは理解しておく必要があります。

入力のほうにもバッファリングの仕組みがありますが、出力とはかなり違った話になるので割愛します。

std::cout や std::ofstream による出力はバッファリングされており2、std::cerr による出力はバッファリングされていません3

std::cout による出力がバッファリングされていても、ここまでのページのプログラムで、期待しない動作になることはなかったと思いますが、これは2つの理由があります。

1つには、バッファに溜まったままになっていたデータは、main関数が終了するときに自動的にフラッシュされるからです4。そのため、出力しようとしたデータは遅くとも、プログラムが終了する前までには出力されます。

【上級】プログラムが異常終了するケースでは、出力されないままになる可能性があります。必ず出力されなければならない重要なメッセージは、バッファリングされないストリームで出力するか、出力のコードを書くときに強制的にフラッシュします。

1つ目の保証があるにしても、std::cin から入力を受ける前に、入力を促すメッセージ(“Please enter the integer.” のような)は、確実にそのタイミングで出力されなければなりません。これについては2つ目の理由によって保証されています。2つ目の理由とは、std::cin による入力の前には std::cout がフラッシュされるということです。

std::cout << "Please enter the integer.\n";  // ここでバッファに蓄えられて、実際に出力されなかったとしても...
std::cin >> value;  // 入力処理の直前で、std::cout の出力内容が自動的にフラッシュされる

なお、std::cerr への出力の前に、std::cout がフラッシュされる保証もあります。

【C++98/03 経験者】std::cerr への出力の前に、std::cout がフラッシュされる保証は、C++11 で追加されたものです5

【上級】2つ目の保証は、2つのストリームを紐づける std::basic_ios::tie()5 によるものです。この関数の効果で、一方のストリームが入出力を行うとき、その直前で他方のストリームのフラッシュが行われるようになります。デフォルトでは、std::cin に std::cout が、std::cerr に std::cout が紐づけられています6

強制的なフラッシュ

std::flush を使って、強制的にフラッシュさせることができます。

std::cout << "error.\n" << std::flush;   // 強制的に出力

これは、“error.¥n” がフラッシュされるという意味ではなく、“error.¥n” も含めて、この時点までにバッファに溜まっていたものがすべてフラッシュされます。ですから、これより手前で出力しようとしていたものも含めて、順番どおりに出力されます。

1つのメッセージは1行分の文字列であることが多いので、改行文字を出力したあと std::flush を置くかたちがよく現れます。この「改行してフラッシュ」というセットは、std::endl でまとめて表現できます。

std::cout << "error." << std::endl;   // 最後に、改行+フラッシュ

処理効率を高めることがバッファリングの目的ですから、ただちに出力されなければならない理由がないかぎり、強制的にフラッシュする必要はありません。

return文

エラーが発生したとき、プログラムの実行を止めなければならないことがあります。プログラムを止める手段はいくつかありますが、もっとも基本的な方法は、main関数の処理を終わらせることです。

main関数を終わらせればいいので、次のようにプログラムを書くことはできます。

int main()
{
    // ...

    if (/*エラーチェック*/) {
        // エラー時の処理
    }
    else {
        // 正常時の処理
    }
}

このような書き方では、エラーが発生した場合には、正常時の処理を通らないように気をつけながら、main関数の末尾まで行きつかせなければなりません。これまでのページで解説してきた方法だけで実現しようとすると、if-else をうまく使って切り分けていくことになりますが、エラーチェックを複数の箇所で行うようになってくると、ネストが繰り返される読みづらいコードになります。

int main()
{
    // ...

    if (/*エラーチェック*/) {
        // エラー時の処理
    }
    else {
        // ...
        if (/*エラーチェック*/) {
            // エラー時の処理
        }
        else {
            // ...
            if (/*エラーチェック*/) {
                // エラー時の処理
            }
            else {
                // ...
            }
        }
    }
}

そこで、一発で main関数を終了させられる return文 (return statement)を使うことにします。

return文の構文は次のとおりです。

return 戻り値;

return文は、実行中の関数を終了させます。いまのところ、自分で書いている関数は main関数だけなので、ほかの使い方を説明できませんが、新しい関数を自作する方法を学ぶと、return文は頻繁に登場するようになります。

戻り値 (return value) というのは、その関数の処理を開始するきっかけを作った側に返す値のことです。main関数の場合は、int型の値を1つ指定します。

「関数の処理を開始するきっかけ」と書きました。関数はどこか別の場所からの呼び出し (call) によって開始されます。main関数はプログラムの開始位置になっているものですから、プログラムを起動したときに呼び出されています。プログラムを Windows のコマンドプロンプトから実行したとすると、コマンドプロンプトがきっかけを作った側ということになります。そのため、main関数の戻り値はコマンドプロンプトに返され、コマンドプロンプトの側でその値を使うことができます。

main関数の戻り値は、0 あるいは 0以外とで意味が異なります。0 はプログラムが正常に終了したことを表しており、0以外は何かしらの問題があって終了したことを表しています。また、0 の代わりに EXIT_SUCCESS、0以外の値の代わりに EXIT_FAILURE と書く方法もあります。EXIT_SUCCESS や EXIT_FAILURE と書く場合には、#include <cstdlib> が必要です。

EXIT_SUCCESSEXIT_FAILURE は整数のようには見えませんが、これらは整数に名前を付けたものです。0 とか 1 はマジックナンバーであり、意味が分かりづらいですから、分かりやすい名前を使おうということです(こうした必要性は「定数式と識別子」のページで取り上げました)。

【上級】コマンドプロンプトから hello.exe を実行するとして、hello.exe が適切な戻り値を返してくれていれば、hello.exe && echo "OK" とか hello.exe || echo "error" のようにして、成功時に OK を出力したり、失敗時に error を出力したりできます。また、バッチファイルを作るとき、処理を分岐させるために使用できます。

これまでのページでは、main関数に return文を書くことはありませんでした。これは、main関数においては return文を省略することが許されているためです。return文に出会わずに末尾の } に処理が到達した場合、そこに return 0; があるものとみなされます4

return文を使って、さきほどのソースコードを次のように整理できます。

#include <cstdlib>

int main()
{
    // ...

    if (/*エラーチェック*/) {
        // エラー時の処理
        return EXIT_FAILURE;
    }

    // ...

    if (/*エラーチェック*/) {
        // エラー時の処理
        return EXIT_FAILURE;
    }

    // ...

    if (/*エラーチェック*/) {
        // エラー時の処理
        return EXIT_FAILURE;
    }

    // ...
}

ファイルオープン時のエラー

ファイルをオープンするときに起こるエラーの理由はさまざま考えられますが、原因まで特定することは、C++ の標準機能だけでは不可能です。

【上級】原因として、指定したパスのファイルが存在しない、オープンする権限がない、同時にオープンできるファイルの数の限界に到達したなどがあります。具体的な原因を知るには OS が提供している API を使う必要があります。

C++ の標準機能としてできることは、「具体的な理由は無視して、エラー発生の有無を調べる」あるいは「結果としてファイルはオープンできたのか調べる」のいずれかです。前者は、これまでにも登場している fail関数を使います。

std::ifstream ifs("test.txt");
if (ifs.fail()) {
    // エラーが起きている
}

ifs.fail() は、!ifs と書いても同じ意味になります。

ファイルがオープンできたのかどうかは is_open関数を使って調べられます。オープンされていたら true、されていなければ false を得られるので、エラーチェックとして使うなら false かどうかを調べます。

std::ifstream ifs("test.txt");
if (ifs.is_open() == false) {
    // オープンできていない
}

どちらの方法でもいいですが、fail関数のほうは、読み書きの際のエラーチェックでも同じ方法が使えます。is_open関数はオープン以外のエラーチェックには使えませんが、何かしらの理由でオープンの状態を調べたいときに使うことができます(クローズを終えると is_open関数の結果は false に戻ります)。

実行できるプログラムにすると、次のようになります。

#include <cstdlib>
#include <fstream>
#include <iostream>

int main()
{
    constexpr auto path = "test.txt";
    std::ifstream ifs(path);
    if (ifs.fail()) {
//  if (ifs.is_open() == false) {    // こちらでもいい
        // オープンに失敗
        std::cerr << "File open error: " << path << "\n";
        return EXIT_FAILURE;
    }

    std::cout << "success.\n";
}

実行結果(test.txt が存在しないとき)

File open error: test.txt

ファイルへの書き込み時のエラー

ファイルへの書き込み時のエラーは、ファイルがオープンできていない、ディスク容量が不足してデータを書けない場合などに起こります。

書き込み時のエラーの確認には fail関数を使います。

注意しないといけないのは、バッファリングの仕組みの存在です。ofs << "Hello" のような書き込みのコードは、データをバッファに貯めているだけかもしれず、その場合はファイル処理に関わるエラーが起こらない可能性が高いです。ところが実際にファイルへ書き込まれるときになって、書き込み権限がないなどの理由でエラーになる可能性があります。ですから、書き込みのエラーチェックは、フラッシュしてから行わないと意味がない可能性があります。

そのため、クローズ時にだけチェックするのも手です。

次のプログラムは書き込みのエラーチェックを行う例です。

#include <cstdlib>
#include <fstream>
#include <iostream>

int main()
{
    constexpr auto path = "test.txt";
    std::ofstream ofs(path);
    if (ofs.fail()) {
        std::cerr << "File open error: " << path << "\n";
        return EXIT_FAILURE;
    }

    ofs << "Hello" << std::endl;  // フラッシュしてから...
    if (ofs.fail()) {             // エラーチェック
        std::cerr << "File write error: " << path << "\n";
        return EXIT_FAILURE;
    }

    std::cout << "success.\n";
}

実行結果(書き込みに失敗した場合)

File write error: test.txt

ファイルからの読み込み時のエラー

ファイルからの読み込み時のエラーは、ファイルがオープンできていない、読み取ったものが想定していた型で表現できない場合などに起こります。

読み込み時のエラーの確認には fail関数を使います。

それに加えて、読み取った内容が期待どおりのものかどうかのチェックは別途必要です。たとえば、読み取った整数が 0以上であることを期待しているなら、その期待どおりであるかどうかをチェックしなければなりません。

次のサンプルプログラムは、読み込み自体のエラーのチェックと、読み取った整数が期待通りであるかどうかのチェックをそれぞれ行っています。

#include <cstdlib>
#include <fstream>
#include <iostream>

int main()
{
    constexpr auto path = "data.txt";
    std::ifstream ifs(path);
    if (ifs.fail()) {
        std::cerr << "File open error: " << path << "\n";
        return EXIT_FAILURE;
    }

    int value1 {};
    int value2 {};
    int value3 {};
    ifs >> value1 >> value2 >> value3;

    if (ifs.fail()) {
        // 読み込みの処理自体でエラーが起きている
        std::cerr << "File read error: " << path << "\n";
        return EXIT_FAILURE;
    }
    if (value1 < 0) {
        // 読み込みの処理自体ではエラーは起きていないが、読み取れたものは想定外
        std::cerr << "File format error: " << path << "\n";
        return EXIT_FAILURE;
    }

    std::cout << value1 << "," << value2 << "," << value3 << "\n";
}

実行結果(エラーが起きた場合):

12 34 56   <-- 入力した内容
File read error: data.txt
-10 10 20   <-- 入力した内容
File format error: data.txt

ファイルクローズ時のエラー

ファイルクローズの処理そのものが失敗することはほとんどありませんが、クローズ時にフラッシュが行われるため、フラッシュの処理に失敗する可能性があります。フラッシュしようとしたがディスクの容量が足りないとか、書き込もうとしていたファイルにアクセスできなくなっていたといったことです。

ファイルのクローズは自動的に行われるため、任せている限り、エラーチェックを行うタイミングがありません。クローズ時のエラーチェックを行うには、クローズを明示的に行ってから、ここまでと同様の方法でチェックします。

#include <cstdlib>
#include <fstream>
#include <iostream>

int main()
{
    constexpr auto path = "test.txt";
    std::ofstream ofs(path);
    if (ifs.fail()) {
        std::cerr << "File open error: " << path << "\n";
        return EXIT_FAILURE;
    }

    ofs << "Hello.\n";

    ofs.close();  // 明示的にクローズ
    if (ifs.fail()) {
        std::cerr << "File close error: " << path << "\n";
        return EXIT_FAILURE;
    }
}

実行結果(エラーが起きた場合):

File close error: test.txt

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次のプログラムを実行すると、どういう出力になりますか?

#include <iostream>

int main()
{
    std::cout << "a";
    std::cerr << "b";
    std::cout << "c" << std::flush;
    return 0;
    std::cout << "d";
    std::cerr << "e";
}

解答・解説

問題2 (応用★★)

標準入力から名前と年齢、血液型を入力させ、その情報をファイルへ書き出すプログラムを作成してください。ファイル処理に関するエラーチェックも実装してください。

解答・解説

問題3 (応用★★)

問題2で作られたファイルを読み込んで、名前と年齢、血液型を標準出力へ出力するプログラムを作成してください。ファイル処理に関するエラーチェックも実装してください。

解答・解説

問題4 (調査★★)

標準出力と標準エラーへ出力しているプログラムを作成し、リダイレクトによって出力先を別々のところに変更することを試してみてください。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る