4

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 into from_span by reference. This works.
  • Returning my_span along with the range from the function, e.g. via a std::unique_ptr to clarify ownership and prevent a copy on return

    auto 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?

cigien
  • 57,834
  • 11
  • 73
  • 112
ulf
  • 190
  • 1
  • 6
  • What happens when you run that first snippet? I'm not sure if there's UB there. – cigien Jun 03 '20 at 01:41
  • 1
    It *appears* to [work](https://godbolt.org/z/YSFD85) for me when I use `ranges::span`. – cigien Jun 03 '20 at 01:47
  • On my system it seems to work most of the time, i.e. prints `[1,2,3,4,5,6,7,8,910]`. Sometimes `Segmentation fault`, sometimes empty lists `[]`. How I came to realize that there was a problem at all is that in the actual code I'm using `return ranges::any_view(ranges::views::all(my_span));` to cast to a common type. In that case it often printed out pretty random numbers and most of the time I was thinking that the error has to do with the any_view. – ulf Jun 03 '20 at 01:48
  • @cigien Thanks, that's interesting, I tried godbolt with gcc10.1 (which I thought had full C++20) support, but couldn't get std::span to work. I'll need a bit of time to track down the difference to my system. Not sure yet and it always takes some time to force the weird behavior for the very simple example I gave on my system. – ulf Jun 03 '20 at 01:54
  • In this particular case you can just return `ranges::views::all(my_array)`, without using `span`. – cpplearner Jun 03 '20 at 02:08
  • @cpplearner good point, that's true. In some places of my actual code, I do need something like span though, since I just want to bind the view to some memory range. – ulf Jun 03 '20 at 02:26
  • Update: the behavior seems to be different when using `ranges::span` vs Martin Moene's span-lite implementation. Using `ranges::span`, I can't force the segfaults I could before. Also, it seems that the mechanics triggered by the return from the `from_span` function are different to before: although the destructor for the `my_span` object is triggered on return (same as before), a new `ranges::span` object is created / copied on return as well which persists until `main` exits. This behavior is *different* from span-lite, where the copy of the span is *not* made when from_span returns. – ulf Jun 03 '20 at 02:38

1 Answers1

6

There range library probably doesn't know that nonstd::span is a view. You need to tell it by specializing ranges::enable_view. Without that, the range library thinks it's something like a vector, and when you pass an lvalue of it to views::all you get back a view that references the local span object rather than a copy of the span.

In the recent past, range-v3 would have used a heuristic to guess (correctly) that span was a view, and your code would have just worked. It changed per request by the C++ Committee, which didn't like the heuristic. To be fair, it would sometimes guess wrong.

Eric Niebler
  • 5,927
  • 2
  • 29
  • 43
  • 2
    Thanks Eric, that fully explains and solves it. When I wrote this question, I was not aware of the `ranges::span` and the entire problem does not occur once I switched to this. Also a huge thanks for the library, my C++ code will start looking very different now. – ulf Jun 04 '20 at 02:40