12

I noticed that std::ranges::sort cannot sort std::vector<bool>:

<source>:6:51: error: no match for call to '(const std::ranges::__sort_fn) (std::vector<bool, std::allocator<bool> >)'
6 |   std::ranges::sort(std::vector{false, true, true});
  |   

Is this allowed? Should we need a specialization of std::ranges::sort for std::vector<bool>? Is there any information about how the committee consider this?

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • 6
    What C++20 *should* do is remove the `std::vector` specialization in favor of `std::bitset` and adopt boost's `boost::dynamic_bitset` – Cory Kramer Aug 14 '20 at 12:21
  • @CoryKramer I agree that the `std::vector` should be deprecated, but `std::bitset` is not a substitute because it needs a compile-time constant size. – François Andrieux Aug 14 '20 at 12:34
  • 1
    If you read the notes in that page (https://en.cppreference.com/w/cpp/container/vector_bool), you notice that `std::vector` iterators are implementation defined and that some algorithms might not works... so my guess it that sorting support would not be required. In practice, sorting bools does not make much sense anyway. – Phil1970 Aug 14 '20 at 12:34
  • 1
    @Phil1970 Yep but `std::sort` works for `std::vector`, this is why I ask the question. – 康桓瑋 Aug 14 '20 at 12:38

1 Answers1

16

As an update, now that zip was adopted for , part of that paper added const-assignment to vector<bool>::reference, which allows that that type to satisfy indirectly_writable, and thus std::ranges::sort on a vector<bool> works in C++23.


Correct.

More generally, std::ranges::sort cannot sort proxy references. The direct reason is that sort requires sortable (surprising, right) which if we follow that chain up requires permutable which requires indirectly_movable_storable which requires indirectly_movable which requires indirectly_writable.

And indirectly_writeable is a very peculiar looking concept.

template<class Out, class T>
  concept indirectly_writable =
    requires(Out&& o, T&& t) {
      *o = std::forward<T>(t);  // not required to be equality-preserving
      *std::forward<Out>(o) = std::forward<T>(t);   // not required to be equality-preserving
      const_cast<const iter_reference_t<Out>&&>(*o) =
        std::forward<T>(t);     // not required to be equality-preserving
      const_cast<const iter_reference_t<Out>&&>(*std::forward<Out>(o)) =
        std::forward<T>(t);     // not required to be equality-preserving
    };

I want to specifically draw your attention to:

const_cast<const iter_reference_t<Out>&&>(*o) = std::forward<T>(t);

Wait, we require const assignability?


This particular issue has a long history. You can start with #573, in which a user demonstrated this problem:

struct C
{
    explicit C(std::string a) : bar(a) {}    
    std::string bar;
};

int main()
{
    std::vector<C> cs = { C("z"), C("d"), C("b"), C("c") };

    ranges::sort(cs | ranges::view::transform([](const C& x) {return x.bar;}));

    for (const auto& c : cs) {
        std::cout << c.bar << std::endl;
    }
}

The expectation of course was that it would print b, c, d, z in that order. But it didn't. It printed z, d, b, c. The order didn't change. The reason here is that because this is a range of prvalues, the elements we're swapping as part of the sort. Well, they're temporaries. This has no effect on cs whatsoever.

This obviously can't work. The user has a bug - they intended to sort the Cs by the bars (i.e. use a projection) but instead they're just sorting the bars (even if the lambda returned a reference, they'd be sorting just the bars and not the Cs anyway -- in this case there is only one member of C anyway but in the general case this is clearly not the intended behavior).

But the goal then is really: how do we make this bug not compile? That's the dream. The problem is that C++ added ref-qualifications in C++11, but implicit assignment has always existed. And implicit operator= has no ref-qualifier, you can assign to an rvalue just fine, even if that makes no sense whatsoever:

std::string("hello") = "goodbye"; // fine, but pointless, probably indicative of a bug

Assigning to an rvalue is really only okay if the ravlue itself handles this correctly. Ideally, we could just check to make sure a type has an rvalue-qualified operator=. Proxy types (such as vector<bool>::reference) would then qualify their assignment operators, that's what we would check, and everybody's happy.

But we can't do that - because basically every type is rvalue-assignable, even if very few types actually meaningfully are. So what Eric and Casey came up with is morally equivalent to adding a type trait to a type that says "I am, legitimately, for real, rvalue-assignable." And unlike most type traits where you would do something like:

template <>
inline constexpr bool for_real_rvalue_assignable<T> = true;

This one is just spelled:

T& operator=(Whatever) const;

Even though the const equality operator will not actually be invoked as part of the algorithm. It just has to be there.

You might ask at this point - wait, what about references? For "normal" ranges (say, vector<int>, the iter_reference_t<Out> gives you int&, and const iter_reference_t<Out>&& is... still just int&. That's why this just works. For ranges that yield glvalues, these const-assignment requirements basically duplicate the normal assignment requirements. The const-assignability issue is _only_for prvalues.


This issue was also the driver of why views::zip isn't in C++20. Because zip also yields a prvalue range and a tuple<T&...> is precisely the kind of proxy reference that we would need to handle here. And to handle that, we would have to make a change to std::tuple to allow this kind of const-assignability.

As far as I'm aware, this is still the direction that it's intended (given that we have already enshrined that requirement into a concept, a requirement that no standard library proxy types actually satisfy). So when views::zip is added, tuple<T&...> will be made const-assignable as well as vector<bool>::reference.

The end result of that work is that:

std::ranges::sort(std::vector{false, true, true});

will actually both compile and work correctly.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Great answer, but would you please elaborate more on the `T& operator=(Whatever) const;` part? I have read the answer almost a dozen times, yet I am still confused by this. Where is it? Why does it help? How does it help? How does it imply that "*I am, legitimately, for real, rvalue-assignable.*"? Where is it checked? – Fureeish Aug 15 '20 at 01:01
  • @Fureeish It's checked by `indirectly_writeable` - the reference type of the iterator must to be `const`-assignable. – Barry Aug 15 '20 at 01:32
  • I fear that I still do not have a good understanding on that, but I do not want to engage into a discussion about such a detail in the comment section. Could you share some pointers / phrases which touch this topic? Maybe I'm just a little confused. – Fureeish Aug 15 '20 at 01:34
  • 1
    @Fureeish See the linked issue and the other issues that link to it? – Barry Aug 15 '20 at 01:42
  • I do, seems like a good place to start, thank you. I forgot that there was an actual link after reading the answer multiple times. – Fureeish Aug 15 '20 at 01:44
  • If I understand correctly, we can just add `const reference& operator=(bool) const noexcept { return *this; }` to `vector::reference` class, then `std::ranges::sort` works. – 康桓瑋 Aug 15 '20 at 11:55
  • 1
    @康桓瑋 Yes. Except I would either declare it as `void operator=(bool) const;` with no definition or actually define it to do the assignment. Having it be a valid function that does nothing seems questionable. – Barry Aug 15 '20 at 13:08
  • What about the ["the move from the indirectly_readable type can be performed via an intermediate object"](https://en.cppreference.com/w/cpp/iterator/indirectly_movable_storable)? I suppose the __unguarded_linear_insert part of the libstdc++ sort implementation relies that a value can be temporarily stored in a local variable, unmodified, and then placed back in the range. But when the proxy holds just a reference, the location where it points gets overwritten. – SD57 Oct 24 '21 at 18:25
  • @SD57 The *value* can be temporarily stored, not the reference. If you have a proxy reference, you still need to have a value type. – Barry Oct 24 '21 at 19:54
  • Is it tuple for the zip iterator, if I understand correctly? – SD57 Oct 25 '21 at 05:49
  • 1
    @SD57 Yep, assuming the reference type is tuple – Barry Oct 25 '21 at 13:13
  • @Barry Do I understand correctly that having tuple as the value type returned by operator*() is sufficient for algorithms like transform, and may be more efficient because it avoids construction of T...? – SD57 Oct 29 '21 at 17:08
  • @SD57 The type returned by `operator*()` is not the value type, it's the reference type. Also I don't understand the question, sufficient for what? And sure, constructing a reference is typically more efficient than constructing an object, but that's not strictly reltaed to tuple. – Barry Oct 29 '21 at 18:21
  • Oh, I see. I got confused because I had the same type (working like a reference) as the value_type and the reference_type of an iterator. I satisfied the std::sortable concept, but worked wrong. Do algorithms that do not require std::indirectly_movable_storable guarantee to not create objects of value_type? – SD57 Oct 29 '21 at 19:11