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

时间:2016-04-24 07:56:39

标签: c++ range-v3

我有一个带有此签名的第三方函数:

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

我还有T个名为src的潜在无限范围(of the range-v3 sort)。我想创建一个管道,将f映射到该范围的所有元素,并将所有向量展平为包含其所有元素的单个范围。

本能地,我会写下以下内容。

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

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

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

6 个答案:

答案 0 :(得分:14)

看起来像there are now test cases in the range-v3 library,它显示了如何正确执行此操作。有必要将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)>);

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

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

答案 1 :(得分:9)

我怀疑它不能。 view没有任何机构可以在任何地方存储临时工具 - 明确反对来自docs的观点概念:

  

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

因此,为了让join能够工作并且比表达式更长久,某些地方的东西必须抓住那些临时工。那个东西可能是action。这可行(demo):

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

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

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

答案 2 :(得分:9)

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

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

如果view::join要存储begin返回的临时向量的endf迭代器,那么它们在被使用之前就会失效。

“这一切都很棒,凯西,但为什么范围内的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 << ',';
    }
}

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

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

因为我们还没有接受它 - 欢迎补丁;)

答案 3 :(得分:5)

<强>被修改

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

我使用ranges::view_facade创建自定义视图。它包含f返回的向量(一次一个),将其更改为范围。这使得可以在一系列此类范围内使用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编译。

答案 4 :(得分:2)

这里的问题当然是视图的整个想法 - 一个非存储的分层惰性求值器。为了跟上这个契约,视图必须传递对范围元素的引用,并且通常它们可以处理rvalue和左值引用。

不幸的是,在这种特定情况下,view::transform只能提供右值引用,因为函数f(T t)按值返回容器,view::join在尝试绑定视图时需要左值({1}} {1}})内部容器。

可能的解决方案都会在管道中的某处引入某种临时存储。以下是我提出的选项:

  • 创建一个view::all版本,可以在内部存储由右值引用传递的容器(如Barry所建议)。从我的角度来看,这违反了 “非存储视图”概念,也需要一些痛苦的模板 编码,所以我建议反对这个选项。
  • view::all步骤之后,将临时容器用于整个中间状态。可以手工完成:

    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 s,但任何更复杂的转换都可以尝试以非直接的顺序访问此view::transform存储。

    在那种情况下,可以使用其他类型的存储,例如, 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中删除以节省内存,则可以进一步改进此方法。然而,这取决于管道的进一步步骤和评估。

由于这3个解决方案几乎涵盖了在std::mapview::transform之间引入临时存储的所有地方,我认为这些都是您拥有的所有选项。我建议使用#3,因为它将允许您保持整体语义完整,并且实现起来非常简单。

答案 5 :(得分:2)

这是另一个不需要太多花哨的黑客攻击的解决方案。这需要在每次拨打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';
    }
}