Search code examples
c++perfect-forwarding

Forwarding the value category of a container?


In C++20 I want to write generic code to take a container or a view and perfectly forward each element. That is, if I have an obnoxious type like this:

struct S {
    struct tag {}; // So we have a way to construct an S.
    S(tag) {}

    S() = delete; // S isn't semiregular.
    S(const S&) = delete;
    S(S&&) = default; // It's movable
    S& operator=(const S&) = delete;
    S& operator=(S&&) = default;
};

I want to be able to pass a std::vector<S> to a function and have it consume the Ses.

I had originally assumed that this would work:

template <typename Range, typename Fn>
void myForEach(Range&& range, Fn f) {
    for (auto&& x : std::forward<Range>(range)) {
        f(std::forward<decltype(x)>(x));
    }
}

but even if FWD(range) is an rvalue, x isn't, so if f needs an S&&, this won't work.

C++23 adds std::forward_like which seems close. In particular:

template <typename Range, typename Fn>
void myForEach(Range&& range, Fn f) {
    for (auto&& x : range) {
        f(std::forward_like<Range>(x));
    }
}

will forward the value category of the range... but what if the range is a view? If Range&& winds up being std::span<S>&&, I don't want the rvalue-ness of the span to apply tho the S! I think (?) we'd want to consider std::ranges::enable_view<Range> and if so, ignore the value category of Range&& and just use the value category from te dereferenced iterator.

Furthermore we'd want this to work for ranges of std::move_iterator and for generators.

Is there any standard C++ functionality for this?

I've found similar questions, but they aren't up-to-date or don't quite get to my point. E.g., Universal Reference of vector


Solution

  • I’ve come to conclude that the core issue is that C++ containers make no attempt to propagate their value category. That is, std::optional<int>&& has a .value() of type int&& but std::vector<int>&& doesn’t have *v.begin() as an int&&. That said…

    I think this answers my question, but it is dangerous and so likely shouldn't be used:

        #include <algorithm>
        #include <ranges>
        #include <type_traits>
        #include <utility>
        #include <vector>
    
    
        #define FWD(...) static_cast<decltype(__VA_ARGS__)&&>(__VA_ARGS__)
    
        // C++23's std::forward_like:
        template<class T, class U>
        [[nodiscard]] constexpr auto&& forward_like(U&& x) noexcept {
            constexpr bool is_adding_const = std::is_const_v<std::remove_reference_t<T>>;
            if constexpr (std::is_lvalue_reference_v<T&&>)
            {
                if constexpr (is_adding_const)
                    return std::as_const(x);
                else
                    return static_cast<U&>(x);
            }
            else
            {
                if constexpr (is_adding_const)
                    return std::move(std::as_const(x));
                else
                    return std::move(x);
            }
        }
    
        // Useful for debugging:
        template <typename> struct SayIt;
    
        //! A forwarding function that does what I'm looking for.
        //! Beware: If std::ranges::enable_view<Range> isn't specialized when it should, this will assume ownership semantics!
        //! Most uses of std::ranges::enable_view<Range> are opt-in, with safe behavior if it's not correclty specialized.
        //! This is dangerous: If you have a veiw that doesn't specialize that, it will treat it as owning.
        template<class Range, class U, class V>
        [[nodiscard]] constexpr auto&& forward_element_of(V&& x) noexcept {
            static_assert(std::is_same_v<U&&, decltype(x)>, "You have to name the type of x explicitly and correctly.");
            static_assert(std::is_same_v<std::remove_const_t<std::remove_reference_t<decltype(*std::begin(std::declval<Range>()))>>, 
                                        std::remove_const_t<std::remove_reference_t<decltype(x)>>>);
            //! If it's a view, ignore the constness and value-category of Range, just look at x:
            if constexpr (std::ranges::enable_view<Range>) {
                return FWD(x);
            } else {
                // It's not a view: 
                if constexpr (std::is_rvalue_reference_v<decltype(x)>) {
                    return std::move(x);
                }
                if constexpr (std::is_rvalue_reference_v<Range>) {
                    return forward_like<std::add_lvalue_reference_t<std::remove_reference_t<Range>>>(FWD(x));
                } else {
                    return forward_like<Range>(FWD(x));
                }
            }
        }
    
    
        template <typename Range>
        [[nodiscard]] auto forwarded_view_elements_of(Range&& range) {
            return FWD(range) | std::views::transform([](auto&& x) -> decltype(auto) {
                return forward_element_of<Range, decltype(x)>(FWD(x));
            });
        }
    
        template <typename Range, typename F>
        void for_each_with_forwarding(Range&& in, F f) {
            for (auto&& x : forwarded_view_elements_of(FWD(in))) {
                f(FWD(x));
            }
        }
    
        // Useful for examples:
        [[nodiscard]] auto move_range_view(auto&& range) {
            return std::ranges::subrange(std::make_move_iterator(std::ranges::begin(range)), 
                                        std::make_move_iterator(std::ranges::end(range)));
        }
    
        int main() {
            // Examples:
            {   // By-value:
                auto foo = std::vector<int>{};
                for_each_with_forwarding(foo, [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&>); });
                // Owning => constness propagates:
                for_each_with_forwarding(std::as_const(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), const int&>); });
                // Owning => move propagates:
                for_each_with_forwarding(std::move(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&&>); });        
            }
            {   // A view:
                auto vec = std::vector<int>{};
                auto foo = std::ranges::subrange(vec); //< A view.
                for_each_with_forwarding(foo, [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&>); });
                // Constness doesn't propagate:
                for_each_with_forwarding(std::as_const(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&>); });
                // Move doesn't propagate:
                for_each_with_forwarding(std::move(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&>); });        
            }
            {   // Transform that just forwards (should do the same as a simple view with std::ranges::subrange).
                auto vec = std::vector<int>{};
                auto foo = vec | std::views::transform([](auto&& x) -> decltype(auto) { return FWD(x); });
                for_each_with_forwarding(foo, [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&>); });
                // Constness doesn't propagate:
                for_each_with_forwarding(std::as_const(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&>); });
                // Move doesn't propagate:
                for_each_with_forwarding(std::move(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&>); });        
            }
            {   // Transform that returns a value:
                auto vv = std::vector<int>{};
                auto foo = vv | std::views::transform([](auto&& x) { return FWD(x); });
                for_each_with_forwarding(foo, [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&&>); });
                // Constness doesn't propagate:
                for_each_with_forwarding(std::as_const(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&&>); });
                // Move doesn't propagate:
                for_each_with_forwarding(std::move(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&&>); });        
            }
            {   // A move view:
                auto vec = std::vector<int>{};
                auto foo = move_range_view(vec);
                for_each_with_forwarding(foo, [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&&>); });
                // Constness doesn't propagate:
                for_each_with_forwarding(std::as_const(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&&>); });
                // Move doesn't propagate:
                for_each_with_forwarding(std::move(foo), [](auto&& x) { static_assert(std::is_same_v<decltype(x), int&&>); });        
            }
        }
    

    https://godbolt.org/z/axs7W3svn

    The function forward_element_of<Range, Elt>(x) checks if std::ranges::enable_view<Range>: If so, it just forwards x. If not, then it assumes it's owned and so propagates the value category and constness of Range. Then

        template <typename Range>
        [[nodiscard]] auto forwarded_view_elements_of(Range&& range) {
            return FWD(range) | std::views::transform([](auto&& x) -> decltype(auto) {
                return forward_element_of<Range, decltype(x)>(FWD(x));
            });
        }
    

    is the useful use, creating a view of elements of that type, constness, and value-category.

    Again, this seems dangerous for a variety of reasons, but I think this is the answer I was looking for. Ultimately, explicitly overloading is much more sensible.