How do I write a range pipeline that uses temporary containers?

I have a third-party function with this signature:

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

I also have an existing potentially infinite range (of the range-v3 sort) of T named src. I want to create a pipeline that maps f to all elements of that range and flattens all the vectors into a single range with all their elements.

Instinctively, I would write the following.

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

However, this won't work, because we cannot create views of temporary containers.

How does range-v3 support such a range pipeline?


It looks like there are now test cases in the range-v3 library that show how to do this correctly. It is necessary to add the views::cache1 operator into the pipeline:

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)>);

so the solutions for the OP's question would be to write

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

range-v3 forbids views over temporary containers to help us avoid the creation of dangling iterators. Your example demonstrates exactly why this rule is necessary in view compositions:

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

If view::join were to store the begin and end iterators of the temporary vector returned by f, they would be invalidated before ever being used.

"That's all great, Casey, but why don't range-v3 views store temporary ranges like this internally?"

Because performance. Much like how the performance of the STL algorithms is predicated on the requirement that iterator operations are O(1), the performance of view compositions is predicated on the requirement that view operations are O(1). If views were to store temporary ranges in internal containers "behind your back" then the complexity of view operations - and hence compositions - would become unpredictable.

"Ok, fine. Given that I understand all of this wonderful design, how do I MAKE THIS WORK?!??"

Since the view composition won't store the temporary ranges for you, you need to dump them into some kind of storage yourself, e.g.:

#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 << ',';
    }
}

Note that the correctness of this approach depends on the fact that view::join is an input range and therefore single-pass.

"This isn't novice-friendly. Heck, it isn't expert-friendly. Why isn't there some kind of support for 'temporary storage materialization™' in range-v3?"

Because we haven't gotten around to it - patches welcome ;)


I suspect it just can't. None of the views have any machinery to store temporaries anywhere - that's explicitly against the concept of view from the docs:

A view is a lightweight wrapper that presents a view of an underlying sequence of elements in some custom way without mutating or copying it. Views are cheap to create and copy, and have non-owning reference semantics.

So in order for that join to work and outlive the expression, something somewhere has to hold onto those temporaries. That something could be an action. This would work (demo):

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

except obviously not for src being infinite, and even for finite src probably adds too much overhead for you to want to use anyway.

You would probably have to copy/rewrite view::join to instead use some subtly modified version of view::all (required here) that instead of requiring an lvalue container (and returning an iterator pair into it), allowed for an rvalue container that it would store internally (and returning an iterator pair into that stored version). But that's several hundred lines' worth of copying code, so seems pretty unsatisfactory, even if that works.