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 S
es.
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
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.