I recently noticed that the following code doesn't work:
#include <ranges>
#include <algorithm>
#include <memory>
#include <iostream>
int main() {
std::cout << *std::ranges::max(
std::views::iota(0, 5) |
std::views::transform([](int i) { return std::make_unique<int>(i); }),
{},
std::ranges::iter_move // another spelling of '[](auto&& p) { return *p; }'
);
}
The reason is that the signature of ranges::max
(the same goes for rangs::min/minmax
) requires indirectly_copyable_storable<iterator_t<R>, range_value_t<R>*>
:
template<input_range R, class Proj = identity,
indirect_strict_weak_order<projected<iterator_t<R>, Proj>> Comp = ranges::less>
requires indirectly_copyable_storable<iterator_t<R>, range_value_t<R>*>
constexpr range_value_t<R>
ranges::max(R&& r, Comp comp = {}, Proj proj = {});
indirectly_copyable_storable
is a less well-known concept used to check whether the value_type
of the iterator can be temporarily stored and reassigned ([alg.req.ind.copy]):
template<class In, class Out>
concept indirectly_copyable_storable =
indirectly_copyable<In, Out> &&
indirectly_writable<Out, iter_value_t<In>&> &&
indirectly_writable<Out, const iter_value_t<In>&> &&
indirectly_writable<Out, iter_value_t<In>&&> &&
indirectly_writable<Out, const iter_value_t<In>&&> &&
copyable<iter_value_t<In>> &&
constructible_from<iter_value_t<In>, iter_reference_t<In>> &&
assignable_from<iter_value_t<In>&, iter_reference_t<In>>;
The first four indirectly_writable
require that Out
can be written by value_type
with all four qualifiers (&
, const&
, &&
, const&&
), where Out
is the artificial range_value_t<R>*
, which is unique_ptr<int>*
in the above example.
Its turns out that this requires the origin range's value_type
must be copyable, so the constraint is not satisfied.
However, the implementation of ranges::max
does not seem to require so many restrictions, which can be seen from the implementation of various compilers (taking MSVC-STL as an example):
range_value_t<_Rng> _Found(*_UFirst);
while (++_UFirst != _ULast) {
if (_STD invoke(_Pred, _STD invoke(_Proj, _Found), _STD invoke(_Proj, *_UFirst))) {
_Found = *_UFirst;
}
}
return _Found;
We just construct the value and reassign it when the condition is met, that's it. I really don't see the part where we need to do:
range_value_t<_Rng>* _Out;
_Out = _Found;
_Out = std::as_const(_Found);
In other words, it seems that just check that the last two conditions of indirectly_copyable_storable
it is enough (the value_type
may also need to be movable).
So where did the four indirectly_writable
come from? Why do ranges::min/max/minmax
all require need such a complex indirectly_copyable_storable
? What are the considerations behind this?
In addition to the ranges::max
family, the only one that indirectly_copyable_storable
is used in <algorithm>
is ranges::unique_copy
, which specifically for input_range
. However, this concept still seems overconstrained in terms of its implementation. So maybe the real question is, how exactly does indirectly_copyable_storable
come out?