Why ranges::actions::join accepts lvalue ranges unless called with pipe syntax?

35 Views Asked by At

I use Range-v3 a lot, but mostly views and algorithms, not very much actions.

As regards ranges::actions::join, why does the following happen?

auto strs{std::vector{"one"s, "twoooooooooooooooooooooooooo"s, "three"s}};
           
auto str1 = join(strs) | to<std::string>;             // join accepts lvalue range                                                                              
auto str2 = std::move(strs) | join | to<std::string>; // join doesn't accept lvalue range

Removing std::move from the last line causes a requirement to fail in action.hpp,

#ifndef RANGES_WORKAROUND_CLANG_43400
            template<typename Rng, typename ActionFn>   // ******************************
            friend constexpr auto                       // ******************************
            operator|(Rng &,                            // ********* READ THIS **********
                      action_closure<ActionFn> const &) // ****** IF YOUR COMPILE *******
                -> CPP_broken_friend_ret(Rng)(          // ******** BREAKS HERE *********
                    requires range<Rng>) = delete;      // ******************************
            // **************************************************************************
            // *    When piping a range into an action, the range must be moved in.     *
            // **************************************************************************
#endif // RANGES_WORKAROUND_CLANG_43400

but I don't really understand why such a requirement exists for actions::join, considering that it is implemented like this:

namespace actions
{
    template<typename Rng>
    using join_action_value_t_ =
        meta::if_c<(bool)ranges::container<range_value_t<Rng>>, //
                   range_value_t<Rng>,                          //
                   std::vector<range_value_t<range_value_t<Rng>>>>;
                                                                              
    struct join_fn
    {
        template(typename Rng)(
            requires input_range<Rng> AND input_range<range_value_t<Rng>> AND
                semiregular<join_action_value_t_<Rng>>)
        join_action_value_t_<Rng> operator()(Rng && rng) const
        {
            join_action_value_t_<Rng> ret;
            auto last = ranges::end(rng);
            for(auto it = begin(rng); it != last; ++it)
                push_back(ret, *it);
            return ret;
        }
    };
                                                                              
    /// \relates actions::join_fn
    /// \sa action_closure
    RANGES_INLINE_VARIABLE(action_closure<join_fn>, join)
} // namespace actions

I mean, a new actual container ret is returned anyway, and rng or its elements is not even std::moved from, so what's the point of requiring that a std::moved range is piped in? And, if there's a valid reason for that, why is the constraint applied at the | level, rather than at the join level (or, in other words, why was the design such that the two lines above compile)?


And I have a somewhat symmetric doubt as regards ranges::actions::transform: this action does transform its input range in place, so I do expect it to require that the |'s lhs range is std::moved,

constexpr auto twice = [](auto s){ return s + s; };
strs = std::move(strs) | transform(twice); // std::move is necessary

but why I'm surprised I can write

strs = transform(strs, twice);
0

There are 0 best solutions below