3

I am trying to pass what I think is a prvalue into a range adapter closure object. It won't compile unless I bind a name to the initializer list and make it an lvalue. What is happening here?

#include <bits/stdc++.h>

using namespace std;

int main(){
  //why does this compile?
  auto init_list = {1,2,4};
  auto v = init_list | views::drop(1);
  
  //but not this?
  // auto v2 = initializer_list<int>{1,2,4} | views::drop(1);

  //or this?
  //auto v3 = views::all(initializer_list<int>{1,2,4}) | views::drop(1);
}
康桓瑋
  • 33,481
  • 5
  • 40
  • 90
danielNJ
  • 43
  • 6
  • 3
    See [Why should I not #include ?](https://stackoverflow.com/q/31816095/3422102) – David C. Rankin Aug 07 '22 at 04:25
  • Initializer lists are used for *initializing* objects. You shouldn't try to treat them as a cheap-and-dirty way to create a sequence of values to manipulate. They do not work well for this purpose. – Nicol Bolas Aug 07 '22 at 04:45
  • Change `initializer_list` to `std::array` and it works https://godbolt.org/z/rPqs5xhxT although sadly not for range-v3 – Tom Huntington Aug 07 '22 at 05:10

2 Answers2

7

When you use r | views::drop(1) to create a new view, range adaptors will automatically convert r into a view for you. This requires that the type of r must model viewable_range:

template<class T>
  concept viewable_­range =
    range<T> &&
    ((view<remove_cvref_t<T>> && constructible_­from<remove_cvref_t<T>, T>) ||
     (!view<remove_cvref_t<T>> &&
      (is_lvalue_reference_v<T> || (movable<remove_reference_t<T>> && !is-initializer-list<T>))));

Note the last requirement:

(is_lvalue_reference_v<T> || (movable<remove_reference_t<T>> && !is-initializer-list<T>))));

Since the type of r doesn't model a view, which in your case is initializer_list, it needs to be an lvalue, allowing us to freely take its address to construct a ref_view without worrying about dangling.

Or, we need it to be movable, which allows us to transfer its ownership to owning_view, which means the following is well-formed:

auto r = std::vector{42} | views::drop(1)

But in this case, we also need it not to be a specialization of initializer_list, because we can't transfer ownership of it, initializer_list always copy. That's why your example fails.

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
0

The situation as of C++23 seems to be (Godbolt): initializer_list is a contiguous range, but not a view and not a borrowed range. This is counterintuitive, since in fact

  • initializer_list is exactly a non-owning view over a range that lives somewhere else; copying an initializer_list is always cheap and always "shallow not deep." So a priori you'd think it's a view.

  • initializer_list's internal state is exactly a pointer and a length and nothing else; it can safely be "deboned" into a pair of pointers. So a priori you'd think it's a "borrowed range."

My hypothesis is that the current wording makes sense primarily because initializer_list is not actually a value type; it's more of a syntactic tag, like std::nullptr_t or std::reference_wrapper. We rarely pass around values of type initializer_list. Instead, they pop into existence to represent a syntactic feature of the call-site. The actual type itself doesn't behave like a well-behaved C++20 Ranges range (if such a thing can be said to exist at all).

template<class R>
auto debone(R rg) {
    static_assert(std::borrowed_range<R>);
    return std::ranges::subrange(rg.begin(), rg.end());
}

auto x = debone<std::initializer_list<int>>({1,2,3,4});
  // fails the static_assert -- good!

If this code didn't fail to compile, then the returned x would contain a pair of iterators into a temporary array (the backing array of the initializer_list) which has already been destroyed. Other "parameter-only types" must deal with the same tradeoff. They generally do advertise borrowed-range-ness, and justify themselves by being a teeny tiny bit harder to construct accidentally from a temporary:

auto x = debone<std::string_view>("hello world");
  // compiles quietly, but also happens to work

auto x = debone<std::string_view>("hello world"s);
  // compiles quietly, and has UB: x.begin() dangles

auto x = debone<std::span<const int>>({{1,2,3}});
  // compiles quietly, and has UB, but will likely work "in practice" (*)

auto x = debone<std::span<const int>>(std::vector{1,2,3}});
  // compiles quietly, and has UB: x.begin() dangles 

(* — See P2752 "Static storage for braced initializers", adopted as a DR in 2023, for why this will likely appear to work in practice. But it's still accessing the backing array outside of its actual lifetime.)

I hypothesize that span and string_view "correctly" advertise themselves as borrowed views because they expect to sometimes be used as value types rather than "parameter-only types" (this is also why they provide operator= and swap, for example). But initializer_list is pretty much a "pure" parameter-only type — it has even been seriously proposed to eliminate its operator=. initializer_list doesn't expect ever to be passed to a Ranges algorithm on purpose. So it doesn't bother to advertise itself as a borrowed range or a view.

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159