I'm a complete beginner with Eric Niebler's ranges-V3 library (which I love so far!), but have been fighting with some problems when returning ranges from from functions. I think I found the problem, but am a bit surprised by the default behavior of the ranges API in this case. Since I did not find any reference to this problem elsewhere and it cost me a fair amount of time, I wrote my problem up somewhat extensively in the hope that this could be helpful to others in the future.
The problem is present in the following minimal example, which leads to undefined behavior.
#include <iostream>
#include "range/v3/all.hpp"
#include "nonstd_span.h"
auto from_span() {
// make this static for the array to persist after the fct returns
static int my_array[10] = { 1,2,3,4,5,6,7,8,9,10 };
auto my_span = nonstd::span<int>(my_array, 10);
return ranges::views::all(my_span);
}
int main() {
std::cout << from_span() << std::endl;
return 0;
}
What I'm trying to achieve: I have some persistent (and constant) contiguous data in my program, that I'm trying to operate on via ranges. The composability, lazy evaluation, together with the non-owning nature of ranges::views made ranges seem like the perfect tool for the task. I want to use the terse syntax that ranges enables, together with passing these as very light, first class objects between functions.
In most code examples demonstrating ranges, the objects that the ranges operate on are created in the same scope as the ranges themselves, hence they are all destructed together once the the range has completed it's evaluation and all is fine.
In my case, the actual data the range operates on is owned externally and I can guarantee that it persists for the lifetime of the range view. For the example above I simply made my_array
static, that the memory range is owned by the function and the data persists once it returns (this may be questionable style, but I believe it is not wrong for the demo).
To create a range from this raw int array, it seems like span is the tool of choice to easily wrap this bare, contiguous data as an iterator to interface with a range view: it is non-owning and light weight. Since some of the compilers I'm using don't support C++20 yet, I used Martin Moene's span-lite instead of std::span
, but had also tested and reproduced the behavior with Tristan Brindle's span library.
The Problem:
I am not certain about this, but I believe the problem with the example above is that in ranges::views::all(my_span)
the range view object does not take ownership of the span object. Although the underlying data (the int array) persists when the range in invoked in the main
function, the my_span
object is destructed as the function exits (I can see the span destructor being called before the view is evaluated). On the platforms and with the various compilers I tested this with (g++ 7.4.0, Clang 6.0.0, MSVC 16.5.5) the code often seems to work, but only because the bits of the former my_span
objects still hang around and have not been overwritten in memory when the range view evaluation is triggered in main
.
The Behavior / API I would have expected
Since span
should be very light weight and ranges::views
are designed to be views of non-owning data, I would have expected the view created by ranges::views::all(my_span)
to take copy the span
object and take ownership of it's copy. This would allow the user not to think about the lifetimes of all the intermediate objects when composing views and pass them around between functions and scopes, as long as the underlying data persists (Maybe my expectations as a naive newbie to ranges are flawed here?). Also when composing new views from other views, does one need to worry about keeping the lower level views alive in case they go out of scope and the new composed view does not?
I tried casting to an r-value reference to trigger the move constructor and force the view to take ownership ranges::views::all(std::move(my_span))
, but this does not seem to be implemented or work.
Some other work-around solutions I have tried:
- Owning
my_span
in the outer scope and passing it intofrom_span
by reference. This works. Returning
my_span
along with the range from the function, e.g. via astd::unique_ptr
to clarify ownership and prevent a copy on returnauto from_span() { using namespace ranges; static int my_array[10] = { 1,2,3,4,5,6,7,8,9,10 }; auto span_ptr = std::make_unique<nonstd::span<int>>(my_array, 10); return std::make_tuple(views::all(*span_ptr), std::move(span_ptr)); } int main() { auto [rng, my_span_ptr] = from_span(); std::cout << rng << std::endl; return 0; }
One could also build a small memory/lifetime management system for spans, that these are owned externally.
None of these solutions seem particularly elegant to me and they would add a large amount of boilerplate code and complexity to the working with range views in this context (shortening the syntax and not having to think about the lifetimes is the very thing I am trying to achieve).
I feel that I am probably missing something here, and that there should be a more elegant solution possible, where the range view takes ownership / copies the light-weight objects (such as span or other views) it was composed of.
Is span
not the right tool for the task? It seems like it was created for use cases such as this one?