我有一个带有此签名的第三方函数:
std::vector<T> f(T t);
我还有T
个名为src
的潜在无限范围(of the range-v3 sort)。我想创建一个管道,将f
映射到该范围的所有元素,并将所有向量展平为包含其所有元素的单个范围。
本能地,我会写下以下内容。
auto rng = src | view::transform(f) | view::join;
但是,这不起作用,因为我们无法创建临时容器的视图。
range-v3如何支持这样的范围管道?
答案 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
返回的临时向量的end
和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 << ',';
}
}
请注意,此方法的正确性取决于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::map
和view::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';
}
}