3

In C++23, it became easier to perfectly forward prvalues thanks to auto() or auto{}. With this new tool, is it now possible to form a FORWARD(e) expression for an expression e with the following requirements?

  1. FORWARD(e) has the same type as e, disregarding reference qualifiers
  2. if decltype(e) is an lvalue/rvalue reference, then FORWARD(e) is an lvalue/xvalue respectively
  3. otherwise, FORWARD(e) has the same value category as e
  4. no additional copying or moving may take place

We can do imperfect forwarding with std::forward already:

#define FORWARD(...) ::std::forward<decltype(__VA_ARGS__)>(__VA_ARGS__)

This preserves the type and converts references appropriately. However, a prvalue will be turned into an xvalue, because std::forward returns an rvalue reference (i.e. FORWARD(e) is an xvalue after conversions).

As a consequence:

T x = T();          // copy elision because x is initialized to prvalue
T x = FORWARD(T()); // calls move constructor

Is it possible to do true perfect forwarding in C++, including preservation of prvalues?

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • 3
    New macros in C++? No thank you. https://stackoverflow.com/q/14041453/1387438 – Marek R Jun 26 '23 at 15:46
  • 2
    Note also that prefect forwarding reliquaries context of function template. So your example lacks crucial details and this one liners do not show anything meaningful. – Marek R Jun 26 '23 at 15:51
  • 1
    So... `#define FORWARD(x) x`? IMO you misunderstand what "forwarding" is. "Forwarding" is fundamentally calling a *function* and having that function preserve the value categories of its arguments. Truly perfect (and transparent) forwarding is still not possible, because a prvalue is not actually data, but a procedure for initializing data. Thus "truly forwarding" a prvalue through a function takes significant work over forwarding a glvalue. – HTNW Jun 26 '23 at 15:54
  • @HTNW the problem with `FORWARD` in terms of a simple `static_cast` or `std::forward` is that it doesn't preserve prvalues, so it isn't universal. For the user, it would be much more convenient if they could just use such a macro to forward expressions, rather than passing a separate type and function argument to `std::forward`. And if the argument happens to be a prvalue, that should also be preserved. – Jan Schultke Jun 26 '23 at 16:15
  • 4
    Is there a use case for forwarding prvalues? – HolyBlackCat Jun 26 '23 at 16:16
  • @HolyBlackCat it's honestly more of a question out of academic interest. Unless you're deep down in maro wizardry, you should always know whether you're forwarding a function parameter (which can't be a prvalue anyways), or some other expression. However, ideally you could define a `FORWARD` macro that doesn't result in unnecessary copies if the operand happens to be a prvalue, unlike most techniques. – Jan Schultke Jun 26 '23 at 16:28
  • `T x = T(); // copy elision because x is initialized to prvalue` - is it true? Isn't it the same as `T x{}` and copy elision does not participate here? – 273K Jun 26 '23 at 16:43
  • @273K This is called "guaranteed copy elision" - it's true in a sense that it's technically not copy elision because there is no copy that is elided, there was just never a copy, but that's the name of the feature (you can think of it as eliding the copy at the language semantic level). – Barry Jun 26 '23 at 16:53
  • "Is there a use case for forwarding prvalues?" Yes. Non-movable things can only be initialized by prvalues, so any container-ish thing hoping to contain them can't use simple perfect forwarding and instead must have emplace-like functionally. More regularity in the language leads to fewer special cases everywhere else. Niche use to be sure, but I've encountered it in practice. – Jeff Garrett Jun 26 '23 at 20:15
  • Is there a SINGLE use of `FORWARD` that will forward both prvalue and xvalue, and the difference is distinguishable (e.g. a "receiver" can tell if it is a prvalue or xvalue) ? – VainMan Jul 10 '23 at 04:22

3 Answers3

5

You basically want FORWARD(e) to be e, except if e happens to name an rvalue reference variable, in which case you want move(e).

You can simply cast to the type of decltype((e)) (which if e is a prvalue, will be elided), except when e is an rvalue reference variable, since decltype((e)) will be an lvalue reference.

#include <type_traits>

template<typename IdType, typename ExprType>
using perfect_forward_cast = std::conditional_t<std::is_rvalue_reference_v<IdType>,
    IdType,
    ExprType
>;

#define FORWARD(...) ::perfect_forward_cast<decltype( __VA_ARGS__ ), decltype(( __VA_ARGS__ ))>( __VA_ARGS__ )

https://godbolt.org/z/WYehMxzPb

Artyer
  • 31,034
  • 3
  • 47
  • 75
  • Thanks for pointing out the conflict, I've fixed it in the question now. I'm pretty sure this answer violates `4.`, because it ultimately boils down to `decltype(__VA_ARGS__)(__VA_ARGS__)` for objects, which is always a copy or move. – Jan Schultke Jun 26 '23 at 16:21
  • @JanSchultke It doesn't copy, `T( prvalue_of_type_T )` is the same as just `prvalue_of_type_T`. This is also how `auto( expr )` works (replacing `auto` with the type of the expression removing references) – Artyer Jun 26 '23 at 16:26
  • Ah yes, great. It passes all tests: https://godbolt.org/z/6468c8oG1 – Jan Schultke Jun 26 '23 at 16:29
3

No.

Given E, we can now do auto(E), but that is always a prvalue. lvalues and xvalues are materialized into a prvalue.

We can also do static_cast<decltype(E)&&>(E) , but that is always a glvalue. prvalues get materialized into an xvalue.

P0849, the proposal that gave us auto(E), had also originally proposed decltype(auto)(E). Such syntax would mean that:

  • if E could be a prvalue, then decltype(auto)(E) would "perfect-forward" E, but would be kind of pointless since here E is already doing the right thing.
  • if E could not be a prvalue (as in, E is the name of a function parameter that is a forwarding reference), then decltype(auto)(E) means the same thing as static_cast<decltype(E)&&>(E).

The latter would provide a way of doing forwarding without either the macro FWD(arg) or the C-style (Arg&&) arg cast that people tend to write. However, decltype(auto)(name) is both longer and also more cryptic than both of these other approaches, so it was dropped from the proposal.

Once you get to a function parameter, you no longer have a prvalue - there's no way to deduce the difference between an xvalue and a prvalue - so there's no real use I can think of to perfect forwarding that also handles prvalues, since you can just use the expression.


The correct way to implement forwarding is:

#define FORWARD(e) static_cast<decltype(e)&&>(e)

not

#define FORWARD(e) std::forward<decltype(e)>(e)

These mean the same thing, but the former avoids instantiating std::forward and some internal type traits there, so is faster to compile.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • `decltype(auto)(e)` is pretty much what I was looking for. It's unfortunate that it didn't make it, but I believe you can imitate it very closely or even perfectly equivalent. See my answer below. – Jan Schultke Jun 26 '23 at 16:23
  • 2
    @JanSchultke What is even the point of any of this? There is no use-case for forwarding prvalues. – Barry Jun 26 '23 at 16:51
2

Yes, it is possible, and it doesn't require C++23. If we can detect the value category of an expression in a macro, we can choose to return this expression from an IILE. This will be subject to mandatory copy initialization, and preserve the prvalue category:

namespace detail {

enum class value_category {
    lvalue,
    xvalue,
    prvalue
};

template <typename T>
constexpr auto category_of(std::remove_reference_t<T>& t)
    -> std::integral_constant<value_category, value_category::lvalue>;

template <typename T>
constexpr auto category_of(std::remove_reference_t<T>&& t)
    -> std::integral_constant<value_category, (std::is_reference_v<T> ? value_category::xvalue : value_category::prvalue)>;

} // namespace detail

These helper functions are extremely similar to std::forward in that they can accept lvalues, xvalues, and prvalues. Instead of returning an rvalue reference of xvalues and prvalues, we instead use these to detect what the value category is.

Now that we have a way of detecting it, we can define the actual FORWARD macro:

#define FORWARD(...) [&]() -> decltype(auto) { \
    constexpr auto c = decltype(::detail::category_of<decltype(__VA_ARGS__)>(__VA_ARGS__))::value; \
    if constexpr (c == ::detail::value_category::prvalue) { \
        return (__VA_ARGS__); \
    } \
    else { \
        return static_cast<decltype(__VA_ARGS__)&&>(__VA_ARGS__); \
    } \
}()

Thanks to if constexpr and a deduced decltype(auto) return type, our IILE can sometimes return references and sometimes values, depending on the detected category.

This preserves types and value categories perfectly. See Compiler Explorer for a live demo.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96