[C++] <ranges>のviewを見る4 - istream_view
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_view
がViewクラスであり次のように定義されています。
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_ios
のoperator 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はどれもシーケンスを生成するもので、他のシーケンスに対して操作を適用したりするものではありません。その振る舞いから、これらのViewはrange factoriesとカテゴライズされます(std::views
にある関数オブジェクトの事も同時に指しているようなので、すこしややこしいですが・・・)。
おそらくrangeライブラリの本命たる、他のシーケンスに対して作用するタイプのViewはrange adaptorsにカテゴライズされ、次回はついにそこに足を踏み入れていきます。
Discussion