如何编写使用临时容器的范围管道?

R. *_*des 55 c++ range-v3

我有这个签名的第三方功能:

std::vector<T> f(T t);
Run Code Online (Sandbox Code Playgroud)

我还有一个已命名的潜在无限范围(范围-v3排序).我想创建一个映射到该范围的所有元素的管道,并将所有向量展平为包含其所有元素的单个范围.Tsrcf

本能地,我会写下面的内容.

 auto rng = src | view::transform(f) | view::join;
Run Code Online (Sandbox Code Playgroud)

但是,这不起作用,因为我们无法创建临时容器的视图.

range-v3如何支持这样的范围管道?

bra*_*ing 17

看起来range-v3 库中现在有测试用例显示如何正确执行此操作。有必要将views::cache1操作符添加到管道中:

auto rng = views::iota(0,4)
        | views::transform([](int i) {return std::string(i, char('a'+i));})
        | views::cache1
        | views::join('-');
check_equal(rng, {'-','b','-','c','c','-','d','d','d'});
CPP_assert(input_range<decltype(rng)>);
CPP_assert(!range<const decltype(rng)>);
CPP_assert(!forward_range<decltype(rng)>);
CPP_assert(!common_range<decltype(rng)>);
Run Code Online (Sandbox Code Playgroud)

所以OP问题的解决方案是写

auto rng = src | views::transform(f) | views::cache1 | views::join;
Run Code Online (Sandbox Code Playgroud)

  • 是的,这就是今天的正确答案。(我是 range-v3 的作者。)对于任何想知道的人来说,“views::cache1”会在视图对象本身内缓存最近生成的范围元素。取消引用迭代器会返回对缓存对象的引用。这给了它一个稳定的地址,因此“views::join”可以迭代它而不会悬空。 (3认同)
  • 无论如何,有没有免费获得这种行为的方法?看起来确实有臭味。也许范围可以用一个特征/概念来增强,该特征/概念表明它们是否是纯的,即:迭代它们两次会得到相同的结果。然后下游组合器可以决定是否缓存。 (3认同)

Bar*_*rry 10

我怀疑它不能.没有view任何机器可以在任何地方存储临时工具 - 这明显违背了文档的观点概念:

视图是一个轻量级的包装器,它以某种自定义方式呈现元素的基础序列视图,而不会发生变异或复制.视图创建和复制起来很便宜,并且具有非拥有的引用语义.

因此,为了join让它能够工作并比表达更长久,某些地方必须要抓住那些临时工.那东西可能是一个action.这可以工作(演示):

auto rng = src | view::transform(f) | action::join;
Run Code Online (Sandbox Code Playgroud)

除了显然不是src无限的,即使是有限的,src可能会增加太多的开销,无论如何都要使用.

您可能必须复制/重写view::join,而不是使用一些精巧修改的版本view::all(此处需要)而不需要左值容器(并将迭代器对返回到其中),允许它将在内部存储的rvalue容器(并返回迭代器对进入该存储的版本).但这是几百行的复制代码,所以看起来非常不令人满意,即使它有效.

  • range 库现在针对此类问题提供了一个干净的解决方案:“views::cache1”。请参阅 Bradgonesurfing 的回答。 (3认同)

Cas*_*sey 9

range-v3禁止查看临时容器,以帮助我们避免创建悬空迭代器.您的示例演示了为什么在视图合成中需要此规则的原因:

auto rng = src | view::transform(f) | view::join;
Run Code Online (Sandbox Code Playgroud)

如果view::join要存储返回的临时向量的迭代器beginend迭代器f,它们将在被使用之前失效.

"这一切都很棒,凯西,但为什么范围-v3视图不会在内部存储这样的临时范围?"

因为表现.就像STL算法的性能是如何根据迭代器操作是O(1)的要求来预测的那样,视图组合的性能取决于视图操作是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 << ',';
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,此方法的正确性取决于view::join输入范围的事实,因此是单通道.

"这不是新手友好的.哎呀,它不是专家友好的.为什么在范围-v3中没有某种'临时存储物化™'的支持?"

因为我们没有得到它 - 欢迎补丁;)

  • 现在的答案是使用“views::cache1”,正如用户 bradgonesurfing 在下面的答案中所示。 (3认同)

ptr*_*trj 5

编辑

显然,下面的代码违反了视图不能拥有他们引用的数据的规则.(但是,我不知道是否严格禁止写这样的东西.)

ranges::view_facade用来创建自定义视图.它包含一个f(一次一个)返回的向量,将其更改为一个范围.这使得可以view::join在一系列这样的范围上使用.当然,我们不能对元素进行随机或双向访问(但是view::join它本身会将范围降级为输入范围),我们也无法分配它们.

struct MyRange从Eric Niebler的存储库中复制了一下,稍微修改了一下.

#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 
Run Code Online (Sandbox Code Playgroud)

用gcc 5.3.0编译.

  • 您的类型声称满足 `View` 概念,因此它可以编译。但是,由于它没有 O(1) 副本,因此该类型不满足 `View` 概念的复杂性要求,因此它/实际上/不是一个 `View`。在实践中,这意味着人们会错误地推断包含“MyRange”的管道的性能。 (3认同)

Eri*_*ler 5

更新

range-v3 现在有views::cache1一个视图,它缓存视图对象本身中的最新元素,并返回对该对象的引用。正如用户@bradgonesurfing 在他的回答中指出的那样,这就是今天这个问题是如何干净有效地解决的。

下面是旧的、过时的答案,为历史好奇而保留。


这是另一种不需要太多花哨黑客攻击的解决方案。它的代价是std::make_shared每次调用 时都会调用f。但是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';
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 那这个呢?`自动 rng = src | 视图::变换(f) | 视图::缓存1 | 视图::加入;` (2认同)