web-dev-qa-db-ja.com

一時的なコンテナーを使用する範囲パイプラインを作成するにはどうすればよいですか?

このシグネチャを持つサードパーティ関数があります。

std::vector<T> f(T t);

また、Tという名前のsrcという既存の無限範囲( 範囲-v3ソート )が存在します。 fをその範囲のすべての要素にマップし、すべてのベクトルをすべての要素を持つ単一の範囲に平坦化するパイプラインを作成したいと思います。

本能的に、私は次のように書きます。

 auto rng = src | view::transform(f) | view::join;

ただし、一時的なコンテナのビューを作成できないため、これは機能しません。

Range-v3はこのような範囲パイプラインをどのようにサポートしますか?

59

できないのではないかと思います。 viewsには、一時をどこにでも保存するための機構はありません。これは docs からのビューの概念に明示的に反しています。

ビューは、要素の基本的なシーケンスのビューを、変更したりコピーしたりせずに、カスタムの方法で表示する軽量ラッパーです。ビューは作成とコピーが簡単で、非所有参照セマンティクスがあります。

したがって、そのjoinが機能し、式より長生きするためには、どこかでこれらの一時的な要素を保持する必要があります。その何かはactionかもしれません。これはうまくいきます( デモ ):

auto rng = src | view::transform(f) | action::join;

明らかにsrcが無限であることを除いて、そして有限のsrcであっても、とにかく使用したいオーバーヘッドが多すぎます。

おそらくview::joinをコピー/リライトして、代わりにview::all(必要な ここ )の微妙に変更されたバージョンを使用する必要がありますその中に)、それが内部的に格納する(そしてその格納されたバージョンにイテレータペアを返す)右辺値コンテナが許可されました。しかし、それは数百行に相当するコードのコピーであるため、たとえそれが機能しても、かなり満足できるものではないようです。

10
Barry

range-v3は、一時的なコンテナのビューを禁止し、ぶら下がりイテレータの作成を回避します。あなたの例は、ビュー構成でこのルールが必要な理由を正確に示しています:

auto rng = src | view::transform(f) | view::join;

view::joinは、beginによって返された一時ベクトルのendおよびfイテレータを格納する必要があり、使用される前に無効化されます。

「それはすばらしいことだ、ケーシー、でもrange-v3ビューはこのような一時的な範囲を内部的に保存しないのはなぜですか?」

パフォーマンスだから。イテレータ操作がO(1)であるという要件に基づいてSTLアルゴリズムのパフォーマンスが予測される方法と同様に、ビュー構成のパフォーマンスは、ビュー操作がO(1)であるという要件に基づいています。ビューが一時的な範囲を「背後」にある内部コンテナに格納する場合、ビューの操作の複雑さ、つまり合成は予測不可能になります。

「わかりました。このすばらしいデザインをすべて理解しているとしたら、どうすればこの作品を作成できますか?!??」

ビューの構成は一時的な範囲を保存しないので、自分でそれらをある種のストレージにダンプする必要があります。例:

#include <iostream>
#include <vector>
#include <range/v3/range_for.hpp>
#include <range/v3/utility/functional.hpp>
#include <range/v3/view/iota.hpp>
#include <range/v3/view/join.hpp>
#include <range/v3/view/transform.hpp>

using T = int;

std::vector<T> f(T t) { return std::vector<T>(2, t); }

int main() {
    std::vector<T> buffer;
    auto store = [&buffer](std::vector<T> data) -> std::vector<T>& {
        return buffer = std::move(data);
    };

    auto rng = ranges::view::ints
        | ranges::view::transform(ranges::compose(store, f))
        | ranges::view::join;

    unsigned count = 0;
    RANGES_FOR(auto&& i, rng) {
        if (count) std::cout << ' ';
        else std::cout << '\n';
        count = (count + 1) % 8;
        std::cout << i << ',';
    }
}

このアプローチの正確さは、view::joinは入力範囲であるため、シングルパスです。

「これは初心者向けではありません。ヘック、エキスパート向けではありません。range-v3で「一時ストレージマテリアライゼーション™」がサポートされていないのはなぜですか?」

私たちはそれに慣れていないので-パッチは歓迎します;)

9
Casey

編集済み

どうやら、以下のコードは、ビューが参照するデータをビューが所有できないというルールに違反しています。 (ただし、このようなものを書くことが厳密に禁止されているかどうかはわかりません。)

ranges::view_facadeを使用してカスタムビューを作成します。 fが返すベクトルを(一度に1つずつ)保持し、範囲に変更します。これにより、そのような範囲の範囲でview::joinを使用できるようになります。確かに、要素へのランダムアクセスまたは双方向アクセスはできません(ただし、view::join自体が範囲を入力範囲に低下させる)ことも、要素に割り当てることもできません。

Eric Nieblerの repository からstruct MyRangeをコピーして、少し変更しました。

#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

std::vector<int> f(int i) {
    return std::vector<int>(static_cast<size_t>(i), i);
}

template<typename T>
struct MyRange: ranges::view_facade<MyRange<T>> {
private:
    friend struct ranges::range_access;
    std::vector<T> data;
    struct cursor {
    private:
        typename std::vector<T>::const_iterator iter;
    public:
        cursor() = default;
        cursor(typename std::vector<T>::const_iterator it) : iter(it) {}
        T const & get() const { return *iter; }
        bool equal(cursor const &that) const { return iter == that.iter; }
        void next() { ++iter; }
        // Don't need those for an InputRange:
        // void prev() { --iter; }
        // std::ptrdiff_t distance_to(cursor const &that) const { return that.iter - iter; }
        // void advance(std::ptrdiff_t n) { iter += n; }
    };
    cursor begin_cursor() const { return {data.begin()}; }
    cursor   end_cursor() const { return {data.end()}; }
public:
    MyRange() = default;
    explicit MyRange(const std::vector<T>& v) : data(v) {}
    explicit MyRange(std::vector<T>&& v) noexcept : data (std::move(v)) {}
};

template <typename T>
MyRange<T> to_MyRange(std::vector<T> && v) {
    return MyRange<T>(std::forward<std::vector<T>>(v));
}


int main() {
    auto src = view::ints(1);        // infinite list

    auto rng = src | view::transform(f) | view::transform(to_MyRange<int>) | view::join;

    for_each(rng | view::take(42), [](int i) {
        std::cout << i << ' ';
    });
}

// Output:
// 1 2 2 3 3 3 4 4 4 4 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 

Gcc 5.3.0でコンパイルされています。

5
ptrj

これは、手の込んだハッキングをあまり必要としない別のソリューションです。 fを呼び出すたびにstd::make_sharedを呼び出すことになります。しかし、とにかくfにコンテナーを割り当ててデータを設定しているので、これは許容できるコストです。

#include <range/v3/core.hpp>
#include <range/v3/view/iota.hpp>
#include <range/v3/view/transform.hpp>
#include <range/v3/view/join.hpp>
#include <vector>
#include <iostream>
#include <memory>

std::vector<int> f(int i) {
    return std::vector<int>(3u, i);
}

template <class Container>
struct shared_view : ranges::view_interface<shared_view<Container>> {
private:
    std::shared_ptr<Container const> ptr_;
public:
    shared_view() = default;
    explicit shared_view(Container &&c)
    : ptr_(std::make_shared<Container const>(std::move(c)))
    {}
    ranges::range_iterator_t<Container const> begin() const {
        return ranges::begin(*ptr_);
    }
    ranges::range_iterator_t<Container const> end() const {
        return ranges::end(*ptr_);
    }
};

struct make_shared_view_fn {
    template <class Container,
        CONCEPT_REQUIRES_(ranges::BoundedRange<Container>())>
    shared_view<std::decay_t<Container>> operator()(Container &&c) const {
        return shared_view<std::decay_t<Container>>{std::forward<Container>(c)};
    }
};

constexpr make_shared_view_fn make_shared_view{};

int main() {
    using namespace ranges;
    auto rng = view::ints | view::transform(compose(make_shared_view, f)) | view::join;
    RANGES_FOR( int i, rng ) {
        std::cout << i << '\n';
    }
}
2
Eric Niebler

もちろん、ここでの問題は、ビューの全体的な概念です。つまり、非保存の階層型レイジーエバリュエーターです。この規約に対応するために、ビューは範囲要素への参照を渡す必要があり、一般に、それらは右辺値と左辺値の両方の参照を処理できます。

残念ながら、この特定のケースでは、_view::transform_は右辺値参照を提供できます。これは、関数f(T t)が値でコンテナーを返すためです。_view::join_は、ビュー(_view::all_)を内部コンテナーに。

可能な解決策はすべて、パイプラインのどこかにある種の一時的なストレージを導入します。ここに私が思いついたオプションがあります:

  • 右辺値参照によって渡されたコンテナを内部に格納できる_view::all_のバージョンを作成します(Barryの提案に従います)。私の観点から見ると、これは「非保存ビュー」の概念に違反しており、また、手間のかかるテンプレートコーディングが必要であるため、このオプションは推奨されません。
  • _view::transform_ステップの後、中間状態全体に一時的なコンテナーを使用します。手で行うことができます:

    _auto rng1 = src | view::transform(f)
    vector<vector<T>> temp = rng1;
    auto rng = temp | view::join;
    _

    または_action::join_を使用します。これは「時期尚早の評価」を引き起こし、無限srcでは機能せず、メモリを浪費し、全体的に元の意図とは完全に異なるセマンティクスを持っているため、それはほとんど解決策ではありませんが、少なくともビュークラスコントラクトに準拠しています。

  • _view::transform_に渡す関数を一時記憶域で囲みます。最も単純な例は

    _const std::vector<T>& f_store(const T& t)
    {
      static std::vector<T> temp;
      temp = f(t);
      return temp;
    }
    _

    次に_f_store_を_view::transform_に渡します。 _f_store_は左辺値参照を返すので、_view::join_は文句を言うことはありません。

    もちろん、これはややハックであり、範囲全体を出力コンテナーのようなシンクにストリームライン化した場合にのみ機能します。 _view::replace_以上の_view::transform_ sなどの簡単な変換には耐えられると思いますが、もっと複雑なものは、このtempストレージに単純でない順序でアクセスしようとする可能性があります。

    その場合、他のタイプのストレージを使用できます。 _std::map_はその問題を修正し、メモリをいくらか犠牲にして無限のsrcおよび遅延評価を許可します。

    _const std::vector<T>& fc(const T& t)
    {
        static std::map<T, vector<T>> smap;
        smap[t] = f(t);
        return smap[t];
    }
    _

    f関数がステートレスである場合、この_std::map_を使用して、一部の呼び出しを潜在的に保存することもできます。要素が不要になることを保証し、メモリを節約するために_std::map_から要素を削除する方法がある場合、このアプローチはさらに改善される可能性があります。ただし、これはパイプラインのさらなるステップと評価に依存します。

これらの3つのソリューションは、_view::transform_と_view::join_の間に一時的なストレージを導入するためのすべての場所をほぼカバーしているため、これらはすべてのオプションだと思います。 #3を使用することをお勧めします。これにより、セマンティクス全体をそのまま維持でき、実装が非常に簡単になります。

2
Ap31