入力反復子と入力範囲

この投稿は、std::getline のいくつかの欠点に触発されました。 前回の投稿で説明した解決策は、間違って実行できないほど単純なインターフェイスなどないことを示しています。または少なくとも準最適。

入力反復子と遅延範囲

前回の記事で std::getline のインターフェースを分析しました より良い代替手段として範囲ベースのソリューションを提案しました。新しい範囲ベースの getlines のユーザー API は次のようにストリームから行を読み取ります:

for(std::string const & line : getlines(std::cin))
{
    use_line(line);
}

getlines から返された範囲オブジェクト 怠け者です;つまり、オンデマンドで行を取得します。良いデザインで、今でも満足しています。ただし、実装には多くの要望が残されています。範囲オブジェクト自体とそれが生成する反復子の両方が、必要以上に太くなっています。 std::istream_iterator について考えさせられました 、および一般的な入力イテレータと範囲。私の結論:std::istream_iterator のような裸の入力イテレータ 範囲に「属さない」ものには重大な問題があります。

ファット入力イテレータ

std::istream_iterator に慣れていない場合 、お気に入りの C++ リファレンスで調べてください。ストリームから抽出したいもののタイプでパラメータ化されています。 istream_iterator<int> int を読み取ります s、istream_iterator<string> string を読み取ります s など。実装は指定されていませんが、要素の読み取りは通常、イテレータが構築されるときに最初に行われ、次にイテレータがインクリメントされるたびに行われます。要素はデータ メンバーに格納されるため、反復子を逆参照するときに要素を返すことができます。ここまででよろしいですか?

istream_iterator<string> の意味 それは、イテレータの巨大な巨獣であるということです。文字列を保持するために太いだけでなく、文字列をコピーすることは文字列をコピーすることも意味します。これは、イテレータをコピーするだけで動的割り当てになる可能性があります! STL アルゴリズムは、通常、イテレータは安価にコピーできると想定し、さりげなく値を取得します。さらに、デフォルトで構築された istream_iterator<string> ダミーのシーケンス終了反復子として使用されます。当然、string が含まれています。 も、しかしそれは決してそれを使用しません! istream_iterator 絶対にダイエットが必要です。これは修正しますが、問題の説明はまだ終わっていません。読み進めてください。

驚くべき副作用

istream_iterator<string> の範囲を返したいとします。 秒。 std::pair を返すことができます それらの、そしてそれはうまくいくでしょう。 boost::iterator_range を返すほうがよいでしょう。 (これは基本的に std::pair です begin の反復子の数 および end メンバー関数) を使用して、ユーザーが範囲ベースの for で反復できるものを取得する ループ:

// Return a lazy range of strings
boost::iterator_range<std::istream_iterator<std::string>>
get_strings( std::istream & sin )
{
    return boost::make_iterator_range(
        std::istream_iterator<std::string>{sin},
        std::istream_iterator<std::string>{}
    );
}

//...

for(std::string const & str : get_strings( std::cin ))
{
    use_string(str);
}

しかし無駄なことを考えてみてください。範囲には 2 つのイテレータがあり、それぞれが文字列とストリームへの参照を保持しています。返された範囲がストリームへの参照を保持し、その begin でオンデマンドで反復子を構築した方が賢明ではないでしょうか? と end 次のようなメンバー関数:

template< class T >
class istream_range
{
    std::istream & sin_;
public:
    using iterator = std::istream_iterator<T>;
    using const_iterator = iterator;

    explicit istream_range( std::istream & sin )
      : sin_(sin)
    {}
    iterator begin() const
    {
        return std::istream_iterator<T>{sin_};
    }
    iterator end() const
    {
        return std::istream_iterator<T>{};
    }
};

OMG、これはとても賢いではありませんか? 範囲オブジェクトは、約 24 バイト (libstdc++ 4.7 を使用) から 4 バイトになりました — ちょうど 1 つのポインターのサイズです! istream_range で遊んでみると 、思われる 仕事に。チェックしてください:

// Read a bunch of strings from a stream
std::istringstream sin{"This is his face"};

for(auto const & str : istream_range<std::string>{sin})
{
    std::cout << str << std::endl;
}

予想どおり、上記の出力は次のとおりです。

This
is
his
face

しかし、すべてがバラではありません。これを見てください:

std::istringstream sin{"This is his face"};
istream_range<std::string> strings{sin};

if(strings.begin() != strings.end())
    std::cout << *strings.begin() << std::endl;

このコードは、範囲が空でないかどうかを確認し、そうである場合は範囲​​の最初の要素を出力します。これは何を印刷すると思いますか? This 、 右?結局のところ、これはストリームの最初の文字列です。試してみると、次のようになります:

is

は?それは、合理的な人が期待することはほとんどありません。この落とし穴を istream_iterator の実装の癖にチョークで書きます .前述のように、ストリームから 1 つを構築すると、ストリームから積極的に値を取得して保存します (または、ほとんどの実装で保存します)。そのイテレータを捨てて、ストリームから 2 番目の値をフェッチする新しいイテレータを作成しない限り、それで問題ありません。 .悲しいことに、上記のコードが行っていることですが、明らかではありません。

std::istream_iterator の最初の問題が肥満だった場合 、2 つ目は、そのコンストラクターには驚くべき副作用があることです。

救助に向かう孤独なレンジャー!

istream_iterator の解決策 これを istream_range に置き換えるのが面倒です .簡単に言えば、ストリームから文字列を読み取る場合、文字列は どこか に存在する必要があります .イテレータの観点から厳密に考えていたとき、イテレータは論理的な場所のように思えました。しかし、範囲を使用すると、範囲オブジェクト内に配置するのにはるかに適した場所になりました。

文字列を範囲オブジェクトに安全に収納することで、太い istream イテレーターの問題をうまく回避できます。イテレータは、範囲へのポインタを保持するだけで済みます。言うまでもなく、イテレータはそれを生成した範囲を超えて存続することはできませんが、それはすべての標準コンテナとそのイテレータに当てはまります。

range オブジェクトはまた、驚くべき副作用を配置するためのより良い場所を提供します:range オブジェクトのコンストラクターです。副作用をイテレータのコンストラクタの外に移動することで、begin でオンデマンドでイテレータを構築することが完全に受け入れられるようになりました。 と end メンバー関数。最適な小さな範囲が残っています — string しかありません そして istream & — そして最適に小さく効率的なイテレータ — ポインタのみを保持します。

これ以上苦労することなく、ここに完全なソリューションがあります:

template< class T >
class istream_range
{
    std::istream & sin_;
    mutable T obj_;

    bool next() const
    {
        return sin_ >> obj_;
    }
public:
    // Define const_iterator and iterator together:
    using const_iterator = struct iterator
      : boost::iterator_facade<
            iterator,
            T const,
            std::input_iterator_tag
        >
    {
        iterator() : rng_{} {}
    private:
        friend class istream_range;
        friend class boost::iterator_core_access;

        explicit iterator(istream_range const & rng)
          : rng_(rng ? &rng : nullptr)
        {}

        void increment()
        {
            // Don't advance a singular iterator
            BOOST_ASSERT(rng_);
            // Fetch the next element, null out the
            // iterator if it fails
            if(!rng_->next())
                rng_ = nullptr;
        }

        bool equal(iterator that) const
        {
            return rng_ == that.rng_;
        }

        T const & dereference() const
        {
            // Don't deref a singular iterator
            BOOST_ASSERT(rng_);
            return rng_->obj_;
        }

        istream_range const *rng_;
    };

    explicit istream_range(std::istream & sin)
      : sin_(sin), obj_{}
    {
        next(); // prime the pump
    }

    iterator begin() const { return iterator{*this}; }
    iterator end() const   { return iterator{};     }

    explicit operator bool() const // any objects left?
    {
        return sin_;
    }

    bool operator!() const { return !sin_; }
};

このソリューションには、std::istream_iterator よりも大きな利点があります。 C++98 の pre-ranges の世界でさえ:イテレータは単一のポインタと同じくらい細身で安価にコピーできます。 istream_iterator のように潜在的に非効率的でエラーが発生しやすいコンポーネントがどのように機能するのか疑問に思う人もいるかもしれません。 そもそもそれを標準にしたことはありません。 (しかし、同じ文で「効率的」と「iostream」について言及したばかりなので、私はどれだけ頭がいいのでしょうね、アンドレイ?)

おまけとして、かわいいコンテキスト変換を bool に追加しました 範囲が空かどうかをテストします。これにより、次のようなコードを記述できます:

if( auto strs = istream_range<std::string>{std::cin} )
    std::cout << *strs.begin() << std::endl;

ブール変換のトリックが気に入らない場合は、古くて退屈な方法でも行うことができます:

istream_range<std::string> strs{std::cin};
if( strs.begin() != strs.end() )
    std::cout << *strs.begin() << std::endl;

strs.begin() に電話できます 好きなだけ何度でも使用でき、厄介な副作用はありません。 getlines を改善するためにこのコードを適応させる 前の投稿からの実装は簡単な演習です。

レンジのホーム

ポストレンジの世界では、istream_range の利点 istream_iterator以上 はさらに明確です。以前の投稿で述べたように、範囲は構成するので素晴らしいです。フィルター、トランスフォーマー、ジッパー、そして範囲アダプターの動物園全体を使用すると、以前は生のイテレーターでは夢にも思わなかったことが、範囲と範囲アルゴリズムで実行できます。

結論

これまでのところ、私が聞いた範囲の議論は、主に範囲の追加の利便性とパワーの観点から組み立てられています.この印象的な利点のリストに、効率性を加えることができます。勝って、勝って、勝って。

Boost.Range ユーザーへの警告

Boost の範囲アダプターの熱心なユーザーは、これをお読みください。それらは現在書かれているため、istream_range との相互作用が不十分です。 こちらで紹介しました。次のように、いくつかのことが機能します:

// read in ints, echo back the evens
auto is_even = [](int i) {return 0==i%2;};
boost::copy( istream_range<int>{std::cin}
               | boost::adaptors::filtered(is_even),
             std::ostream_iterator<int>(std::cout) );

そして、次のように失敗するものもあります:

// read in ints, echo back the evens
auto is_even = [](int i) {return 0==i%2;};
auto evens = istream_range<int>{std::cin}
               | boost::adaptors::filtered(is_even);
boost::copy( evens, std::ostream_iterator<int>(std::cout) );

問題は、一時的な istream_range<int> 反復する前に範囲外になります。 iterator_range< std::istream_iterator<int> > で行っていたら 、実際には機能していたでしょうが、現在の Boost.Range 実装の癖のためだけです。 Boost.Range アダプターは、(A) 適応範囲がたまたま左辺値である場合、または (B) 範囲の反復子がその範囲を超えて存続できる場合にのみ機能します。これらの理想的ではない仮定は、C++98 では意味がありましたが、C++11 では意味がありませんでした。最新のコンパイラでは、Boost.Range は適合された右辺値範囲のコピーを保存でき、保存する必要があります。私の意見では、現代の世界に対応する範囲ライブラリの時期です。

x