[C++] <ranges>のviewを見る4 - istream_view

公開:2020/10/16
更新:2020/10/16
4 min読了の目安(約3700字TECH技術記事

istream_view

istream_view<T>は任意のistreamが示す入力ストリーム上にあるTの値のシーケンスを生成するViewです。

#include <ranges>

int main() {
  // 標準入力に"1 2 3 4 5 6"のように入力したとする
  for (int n : std::ranges::istream_view<int>(std::cin)) {
    std::cout << n; // 123456
  }
}

要はstd::istream_iterator<range>におけるViewとして再設計したものです。

その特性上istream_viewは常にinput rangeとなり、一方向性でマルチパス保証がありません。そのためistream_viewのイテレータはムーブオンリーです。

basic_istream_view

istream_view<T>というのは実はViewクラスではなくてrange factoryに対応する関数です。istream_viewの実体はbasic_istream_viewというクラスで、istream_viewは任意のistreamを引数に受け取り、それを用いてbasic_istream_viewオブジェクトを構築して返します。

このbasic_istream_viewViewクラスであり次のように定義されています。

namespace std::ranges {

  template<movable Val, class CharT, class Traits>
    requires default_initializable<Val> &&
             stream-extractable<Val, CharT, Traits>
  class basic_istream_view : public view_interface<basic_istream_view<Val, CharT, Traits>> {
  public:
    basic_istream_view() = default;
    constexpr explicit basic_istream_view(basic_istream<CharT, Traits>& stream);

    constexpr auto begin()
    {
      if (stream_) {
        *stream_ >> object_;
      }
      return iterator{*this};
    }

    constexpr default_sentinel_t end() const noexcept;

  private:
    struct iterator;                                    // 説明専用メンバ変数
    basic_istream<CharT, Traits>* stream_ = nullptr;    // 説明専用メンバ変数
    Val object_ = Val();                                // 説明専用メンバ変数
  };
}

basic_istream_viewは3つのテンプレートパラメータ(シーケンスの要素型、ストリームの文字型、文字型のtraits)を受け取るのですが、最初の要素型Valを必ず指定しなければいけないためにテンプレート引数推論を行えず、これを直接使おうとすると3つのテンプレートパラメータ全てを指定しなければなりません。

これは地味に面倒なので利用するときはstd::ranges::istream_view<T>()を用いるといいでしょう(なお、std::views::istream_viewはありません)。これは、要素型だけを指定して入力ストリームを渡せば、残り2つのテンプレートパラメータは引数のistreamの型から取得して補ってくれます。
なお、この記事ではbasic_istream_viewによるViewを指してistream_viewと呼びます。

遅延評価

上記定義を見るとピンとくるかもしれませんがistream_viewは遅延評価されます。istream_viewによって生成されるシーケンスはistream_viewオブジェクトを構築した時点では生成されていません。

まず、istream_viewオブジェクトからbegin()によってイテレータを取得した時点で最初の要素が計算(読み取り)されます。そして、インクリメント(++i/i++)のタイミングで1つづつ後続の要素が計算されます。

int main() {
  // 標準入力に"1 2 3 4 5 6"のように入力したとする

  auto iv = std::ranges::istream_view<int>(std::cin); // この段階ではまだ何もしてない

  auto it = std::ranges::begin(iv); // この段階で最初の要素(1)が読み取られる

  int n1 = *it; // 最初の要素(1)が得られる
  ++it;         // ここで次の要素(2)が読み取られる
  it++;         // ここで次の要素(3)が読み取られる
  int n2 = *it; // 3番目の要素(3)が得られる  
}

この事によって、istream_viewによるシーケンスは通常のシーケンスとは異なりメモリ上に空間的に存在するのではなく、ストリーム上に時間的に存在しています。すなわち、istream_viewを構築したタイミングで入力データが全て到着している必要はなく、任意のタイミングで到着しても構いません。
この事は、C#におけるLINQに対するRxの対応と同じです。

シーケンス終端の判定は、ストリーム上にデータが残っているかによって行われます(std::basic_iosoperator boolが用いられます)。入力が無い状態でイテレータをインクリメントするとおそらくブロックすることになります。

int main() {
  // 標準入力に"1 2"のように入力したとする

  auto iv = std::ranges::istream_view<int>(std::cin);
  auto it = std::ranges::begin(iv); // この段階で最初の要素(1)が読み取られる
  auto fin = std::ranges::end(iv);

  it == fin;  // false
  ++it;       // ここで次の要素(2)が読み取られる、ストリーム上のデータはなくなる

  it == fin;  // true
  ++it;       // 次のデータの到着まで待機する(ストリーム実装によるかもしれない?
}

range factories -> range adaptors

ここまでで4つのViewクラス(empty_view, single_view, iota_view, istream_view)を見てきました。これらのViewはどれもシーケンスを生成するもので、他のシーケンスに対して操作を適用したりするものではありません。その振る舞いから、これらのViewrange factoriesとカテゴライズされます(std::viewsにある関数オブジェクトの事も同時に指しているようなので、すこしややこしいですが・・・)。

おそらくrangeライブラリの本命たる、他のシーケンスに対して作用するタイプのViewrange adaptorsにカテゴライズされ、次回はついにそこに足を踏み入れていきます。