C++: while (std::cin);の意味

概要

競技プログラミングなどでC++で標準入力の処理方法を調べていて気になった。以下のようなコードでwhile (std::cin);で標準入力がなくなるまで (データの終端 EOF (End Of File)を検知するまで) ループして処理したりしていた。

#include <iostream>
#include <string>

int main(void) {
  std::string str;
  while (std::cin >> str) {
    std::cout << str << "\n";
  }
  return 0;
}

思えば,C++を始めたときはこのstd::cinとstd::coutがC言語と全然違って困惑して始まり,結局わからないところをそのままにして進んできた。

いろいろ知識もついてきて,気になったのでちゃんと調べることにした。調べるにあたって,参照規格にはC++ 2011のオープンアクセス可能なドラフトのN3337を使用する。

std::cinの定義

まず,std::cinの定義を確認する。規格では「27.4 Standard iostream objects」に以下のようにstd::cinの定義がある。

Header <iostream> synopsis
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>

namespace std {
  extern istream cin;
  extern ostream cout;
  extern ostream cerr;
  extern ostream clog;

  extern wistream wcin;
  extern wostream wcout;
  extern wostream wcerr;
  extern wostream wclog;
}

実際に,Ubuntu 18.04/usr/include/c++/7/iostreamで上記定義を確認できた。

従って,std::cinはistream型のオブジェクトのようだ。さらに,istreamはbasic_istream<char>の別名であることが,「27.7 Formatting and manipulators」の<istream>の定義から分かる。

Header <istream> synopsis
namespace std {
  template <class charT, class traits = char_traits<charT> >
    class basic_istream;
  typedef basic_istream<char>     istream;
  typedef basic_istream<wchar_t> wistream;

  template <class charT, class traits = char_traits<charT> >
    class basic_iostream;
  typedef basic_iostream<char>    iostream;
  typedef basic_iostream<wchar_t> wiostream;

  template <class charT, class traits>
    basic_istream<charT,traits>& ws(basic_istream<charT,traits>& is);

  template <class charT, class traits, class T>
    basic_istream<charT, traits>&
    operator>>(basic_istream<charT, traits>&& is, T& x);
}

つまり,std::cinはbasic_istream<char>のインスタンスとみなせばよい。

std::basic_istream

そのままbasic_istreamの定義を見ていく。

27.7.2 Input streams」で以下のように演算子が定義されている。

namespace std {
  template <class charT, class traits = char_traits<charT> >
  class basic_istream : virtual public basic_ios<charT,traits> {
  public:
    // types (inherited from basic_ios ([ios])):
    typedef charT                     char_type;
    typedef typename traits::int_type int_type;
    typedef typename traits::pos_type pos_type;
    typedef typename traits::off_type off_type;
    typedef traits                    traits_type;

    // [istream.cons] Constructor/destructor:
    explicit basic_istream(basic_streambuf<charT,traits>* sb);
    virtual ~basic_istream();

    // [istream::sentry] Prefix/suffix:
    class sentry;

    // [istream.formatted] Formatted input:
    basic_istream<charT,traits>& operator>>(
      basic_istream<charT,traits>& (*pf)(basic_istream<charT,traits>&));
    basic_istream<charT,traits>& operator>>(
			basic_ios<charT,traits>& (*pf)(basic_ios<charT,traits>&));
    basic_istream<charT,traits>& operator>>(
      ios_base& (*pf)(ios_base&));

    basic_istream<charT,traits>& operator>>(bool& n);
    basic_istream<charT,traits>& operator>>(short& n);
    basic_istream<charT,traits>& operator>>(unsigned short& n);
    basic_istream<charT,traits>& operator>>(int& n);
    basic_istream<charT,traits>& operator>>(unsigned int& n);
    basic_istream<charT,traits>& operator>>(long& n);
    basic_istream<charT,traits>& operator>>(unsigned long& n);
    basic_istream<charT,traits>& operator>>(long long& n);
    basic_istream<charT,traits>& operator>>(unsigned long long& n);
    basic_istream<charT,traits>& operator>>(float& f);
    basic_istream<charT,traits>& operator>>(double& f);
    basic_istream<charT,traits>& operator>>(long double& f);

    basic_istream<charT,traits>& operator>>(void*& p);
    basic_istream<charT,traits>& operator>>(
      basic_streambuf<char_type,traits>* sb);

    // [istream.unformatted] Unformatted input:
    streamsize gcount() const;
    int_type get();
    basic_istream<charT,traits>& get(char_type& c);
    basic_istream<charT,traits>& get(char_type* s, streamsize n);
    basic_istream<charT,traits>& get(char_type* s, streamsize n,
                                     char_type delim);
    basic_istream<charT,traits>& get(basic_streambuf<char_type,traits>& sb);
    basic_istream<charT,traits>& get(basic_streambuf<char_type,traits>& sb,
                                    char_type delim);

    basic_istream<charT,traits>& getline(char_type* s, streamsize n);
    basic_istream<charT,traits>& getline(char_type* s, streamsize n,
                                         char_type delim);

    basic_istream<charT,traits>& ignore(
      streamsize n = 1, int_type delim = traits::eof());
    int_type                     peek();
    basic_istream<charT,traits>& read    (char_type* s, streamsize n);
    streamsize                   readsome(char_type* s, streamsize n);

    basic_istream<charT,traits>& putback(char_type c);
    basic_istream<charT,traits>& unget();
    int sync();

    pos_type tellg();
    basic_istream<charT,traits>& seekg(pos_type);
    basic_istream<charT,traits>& seekg(off_type, ios_base::seekdir);

  protected:
    basic_istream(const basic_istream& rhs) = delete;
    basic_istream(basic_istream&& rhs);

    // [istream.assign] Assign/swap:
    basic_istream& operator=(const basic_istream& rhs) = delete;
    basic_istream& operator=(basic_istream&& rhs);
    void swap(basic_istream& rhs);
  };

  // [istream::extractors] character extraction templates:
  template<class charT, class traits>
    basic_istream<charT,traits>& operator>>(basic_istream<charT,traits>&,
                                            charT&);
  template<class traits>
    basic_istream<char,traits>& operator>>(basic_istream<char,traits>&,
                                           unsigned char&);
  template<class traits>
    basic_istream<char,traits>& operator>>(basic_istream<char,traits>&,
                                           signed char&);

  template<class charT, class traits>
    basic_istream<charT,traits>& operator>>(basic_istream<charT,traits>&,
                                            charT*);
  template<class traits>
    basic_istream<char,traits>& operator>>(basic_istream<char,traits>&,
                                           unsigned char*);
  template<class traits>
    basic_istream<char,traits>& operator>>(basic_istream<char,traits>&,
                                           signed char*);
}

basic_istream<charT,traits>& operator>>(bool& n);のように,>>演算子が型ごとにたくさん定義されており,返却値の型がどれもbasic_istream&となっている。演算子の定義だが,やっていることは関数とほぼ違いがない。

istream (basic_istream) 自身への参照を返してくれているため,以下のような処理の連結が可能となっている。

std::cin >> c1 >> x >> c2 >> y >> c3;

続いて,std::cinをそのまま評価した場合にどうなるかがポイントとなる。そのまま素直に考えると,変数が空かどうかでの判定となってしまうが,これだとデータの終了を検知できない。

もう少し,basic_istreamの説明を読み進めると以下の説明がある。

The class basic_istream defines a number of member function
signatures that assist in reading and interpreting input from sequences
controlled by a stream buffer.

Two groups of member function signatures share common properties: the
formatted input functions (or extractors) and the unformatted input
functions. Both groups of input functions are described as if they
obtain (or extract) input characters by calling rdbuf()->sbumpc() or
rdbuf()->sgetc(). They may use other public members of istream.

If rdbuf()->sbumpc() or rdbuf()->sgetc() returns traits::eof(),
then the input function, except as explicitly noted otherwise, completes
its actions and does setstate(eofbit), which may throw
ios_base::failure ([iostate.flags]), before returning.

If one of these called functions throws an exception, then unless
explicitly noted otherwise, the input function sets badbit in error
state. If badbit is on in exceptions(), the input function rethrows the
exception without completing its actions, otherwise it does not throw
anything and proceeds as if the called function had returned a failure
indication.

27.7.2 Input streams

規格の文章で少々読みにくい。ひとまず翻訳すると以下のとおりとなる。

まず,<istream>
で提供されるbasic_istreamは書式化入力関数と非書式化入力関数の2種類のメンバー関数グループを持つ。>>演算子が書式化入力
関数で,それ以外のgcount, get, getline, ignore, peek, read, readsome, putback,
unget, sync, tellg, seekgが非書式化入力関数となる。

書式化入力関数と非書式化入力関数の両方は (内部で) rdbuf()->sbumpc()rdbuf()->sgetc()関数の呼び出しにより入力文字列が提供される。そして,これらの2種類の関数群はrdbuf()->sbumpc()などの他にbasic_istreamの他のpublic関数も使う。

2種類の関数群で使われるrdbuf()->sbumpc()またはrdbuf()->sgetc()traits::eof()を返す場合,入力関数は処理を完了させ,setstate(eofbit)を行う。setstate(eofbit)は返却の前にios_base::failure()を投げるかもしれない。

rdbuf()->sbumpc()またはrdbuf()->sgetc()のいずれかが,例外を投げる場合,入力関数はエラー状態にbadbitをセットする。もしexceptions()中にbadbitがオンの場合,入力関数が処理を完了させずに例外を再度投げる。オンでない場合,呼び出された関数は失敗表示を返したかのように継続する。

もう少し,手短に説明する。

>>演算子とその他のgetなどの関数をそれぞれ書式化入力関数と非書式化入力関数とグループ分けしている。これらの関数にはデータ処理の共通呼び出し関数があり,EOFを検知するとsetstate(eofbit)が呼ばれるとのことだ。

このsetstate()はbasic_istreamの派生元クラスのbasic_iosで定義されている関数だ (27.5.5.4 basic_iso flags functions)。setstateは中でclear()を読んでいる。このclear()の処理内容がわかりにくい。

void clear(iostate state = goodbit);

Postcondition: If rdbuf()!=0 then state == rdstate(); otherwise rdstate()==(state | ios_base::badbit).

Effects: If ((state | (rdbuf() ? goodbit : badbit)) & exceptions())
== 0, returns. Otherwise, the function throws an object fail of class
basic_ios::failure ([ios::failure]), constructed with
implementation-defined argument values.

void setstate(iostate state);

Effects: Calls clear(rdstate() | state) (which may throw basic_ios::failure ([ios::failure])).

27.5.5.4 basic_iso flags functions

basic_ios::clear – cpprefjp C++日本語リファレンス」も参考にすると,関数を実行するとPostconditionに記載されている状態になる。つまり,rdstate()stateのフラグを追加する。もっというと,setstateで指定した状態が現在の状態に追加される (規格の書き方で言葉での説明が少なくわかりにくい)。

std::basic_istreamboolへのキャスト結果

では,このstateはどこで使われるのか?

basic_iosには他に以下の演算子が上書き定義されている。

explicit operator bool() const;
Returns: !fail().
bool operator!() const;
Returns: fail()


bool fail() const;
Returns: true if failbit or badbit is set in rdstate().302

27.5.5.4 basic_iso flags functions

条件式などでboolへ型変換時の演算子が定義されている。ここで,fail()を使っており,その中のrdstate()でstateを使っているようだ。

肝心のfail()はrdstateにfailbitかbadbitが存在するとtrueを返す。ただ,この情報だけだとEOFの情報 (eofbit) を使っていない。

念の為以下のコードでwhile (std::cin >> str)の終端処理後のstateを確認した。

istream.cpp
#include <iostream>
#include <string>

int main(void) {
  std::string str;
  while (std::cin >> str) {
    std::cout << str << "\n";
  }

  std::cout << "rdstate()=" << std::cin.rdstate() << "\n";
  std::cout << "badbit=" << std::ios_base::badbit
    << ", failbit=" << std::ios_base::failbit
    << ", eofbit=" << std::ios_base::eofbit
    << ", goodbit=" << std::ios_base::goodbit << "\n";

  return 0;
}

以下のコマンドでコンパイルして実行する。

g++ istream.cpp && ./a.out <<-EOT
LINE1
LINE2
EOT

実行結果は以下のとおりだ。

LINE1
LINE2
rdstate()=6
badbit=1, failbit=4, eofbit=2, goodbit=0

どうやら,データの終端になると,eofbitの他にfailbitも存在している。ここまで読んだbasic_istreamの規格だとこれだと説明がつかない。

std::cin::operator >>の処理結果

規格を更に読み進める。「27.7.2.2.3 basic_istream::operator>>」に>>の定義の記載がある。

template<class charT, class traits>
basic_istream<charT,traits>& operator>>(basic_istream<charT,traits>& in,
charT& c);
template<class traits>
basic_istream<char,traits>& operator>>(basic_istream<char,traits>& in,
unsigned char& c);
template<class traits>
basic_istream<char,traits>& operator>>(basic_istream<char,traits>& in,
signed char& c);

Effects: Behaves like a formatted input member (as described in 27.7.2.2.1) of in. After a sentry object is constructed a character is
extracted from in, if one is available, and stored in c. Otherwise, the function calls in.setstate(failbit).
Returns: in.


basic_istream<charT,traits>& operator>>
(basic_streambuf<charT,traits>* sb);
Effects: Behaves as an unformatted input function (as described in 27.7.2.3, paragraph 1). If sb is null, calls setstate(failbit), which may throw ios_base::failure (27.5.5.4).

27.7.2.2.3 basic_istream::operator>>

文字列やバッファーが存在しなければ,setstate(failbit)を実行するときちんと書いてあった。これで説明がついた。

結論

C++でのwhile (std::cin);の意味について調査した。

整理すると,以下の流れでデータの終端または失敗を判定している。

while (std::cin);の意味
  1. std::cin::operator >>実行
  2. データ終端時にsetstate(failbit)setstate(eofbit)を実行
  3. boolへのキャスト時に!fail()を実行
  4. fail()内でrdstate()によりfailbtの有無を判定

わざわざ規格に当たらなくても,C++ ポケットリファレンスの「p. 249 入力の終わり (EOF) を判定する」と「p. 254 ストリームのエラーを知る」を読めば分かることだった。ポケットリファレンスは持っているのだが,規格で調べ終わってから記述に気付いてしまい,後の祭りだった。

C++ポケットリファレンスのSTLの関数定義などの記述がいまいち小難しく見えて,避けていたのがここにきて仇となった。もう少し活用していきたい。

今回の調査で,std::cinの実態がstd::basic_istreamであることを初めて知った。その他,キャストのオーバーライドなどやっていることを初めて知った。C++はいちいち細かいところの調査に規格をあちこち探す必要があり,いちいち時間がかかる。今回の調査も4-5時間かかった。割り切りも必要だが,重要なところなどはきっちり調べて正確な知識を身につけていきたい。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です