你能完美地转发C++23中的表达式吗?

huangapple go评论88阅读模式
英文:

Can you perfectly forward expressions in C++23?

问题

在C++23中,通过auto()auto{},完美转发prvalues变得更容易。使用这个新工具,现在可以满足以下要求形成FORWARD(e)表达式,其中e是一个表达式:

  1. FORWARD(e)的类型与e相同,忽略引用限定符
  2. 如果decltype(e)是左值/右值引用,则FORWARD(e)分别是左值/xvalue
  3. 否则,FORWARD(e)具有与e相同的值类别
  4. 不能进行额外的复制或移动

我们已经可以使用std::forward进行不完美的转发:

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

这会保留类型并适当地转换引用。
然而,prvalue将变成xvalue,因为std::forward返回右值引用(即FORWARD(e)在转换后是xvalue)。

因此:

T x = T();          // 复制省略,因为x初始化为prvalue
T x = FORWARD(T()); // 调用移动构造函数

在C++中是否可能进行真正的完美转发,包括保留prvalues?

英文:

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&lt;decltype(__VA_ARGS__)&gt;(__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?

答案1

得分: 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__ )

Link to code on Godbolt

英文:

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 &lt;type_traits&gt;

template&lt;typename IdType, typename ExprType&gt;
using perfect_forward_cast = std::conditional_t&lt;std::is_rvalue_reference_v&lt;IdType&gt;,
    IdType,
    ExprType
&gt;;

#define FORWARD(...) ::perfect_forward_cast&lt;decltype( __VA_ARGS__ ), decltype(( __VA_ARGS__ ))&gt;( __VA_ARGS__ )

https://godbolt.org/z/WYehMxzPb

答案2

得分: 3

没有。

给定 E,现在我们可以执行 auto(E),但它总是一个 prvalue。lvalues 和 xvalues 被实例化为 prvalue。

我们也可以执行 static_cast<decltype(E)&&>(E)†,但它总是一个 glvalue。prvalues 被实例化为 xvalue。

P0849,提供了 auto(E) 的提案,最初也提出了 decltype(auto)(E)。这样的语法意味着:

  • 如果 E 可以是 prvalue,那么 decltype(auto)(E) 将 "完美转发" E,但在这里 E 已经做了正确的事情,所以有点没有意义。
  • 如果 E 不能是 prvalue(例如,E 是一个转发引用的函数参数的名称),那么 decltype(auto)(E) 的含义与 static_cast<decltype(E)&&>(E) 相同。

后者提供了一种在不使用宏 FWD(arg) 或人们倾向于编写的 C 风格 (Arg&&) arg 转换的情况下进行转发的方法。然而,decltype(auto)(name) 既更长,也比这两种其他方法更加晦涩,因此它被从提案中删除。

一旦你进入函数参数,你就不再有 prvalue - 没有办法推断 xvalue 和 prvalue 之间的区别 - 所以我想不出可以同时处理 prvalues 的完美转发的实际用途,因为你可以直接使用表达式。

†正确的实现转发的方式是:

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

而不是

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

它们的含义相同,但前者避免了实例化 std::forward 和某些内部类型特性,因此编译速度更快。

英文:

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&lt;decltype(E)&amp;&amp;&gt;(E) <sup>&dagger;</sup>, 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&lt;decltype(E)&amp;&amp;&gt;(E).

The latter would provide a way of doing forwarding without either the macro FWD(arg) or the C-style (Arg&amp;&amp;) 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.


<sup>&dagger;</sup>The correct way to implement forwarding is:

#define FORWARD(e) static_cast&lt;decltype(e)&amp;&amp;&gt;(e)

not

#define FORWARD(e) std::forward&lt;decltype(e)&gt;(e)

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

答案3

得分: 2

是的,这是可能的,而且不需要 C++23。
如果我们可以检测宏中表达式的值类别,我们可以选择从 IILE 返回这个表达式。
这将受到强制的复制初始化的限制,并保留 prvalue 类别:

namespace detail {

enum class value_category {
    lvalue,
    xvalue,
    prvalue
};

template &lt;typename T&gt;
constexpr auto category_of(std::remove_reference_t&lt;T&gt;&amp; t)
    -&gt; std::integral_constant&lt;value_category, value_category::lvalue&gt;;

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

} // namespace detail

这些辅助函数与 std::forward 极为相似,可以接受 lvalues、xvalues 和 prvalues。与返回 xvalues 和 prvalues 的右值引用不同,我们使用它们来检测值类别。

现在我们有了一种检测的方法,我们可以定义实际的 FORWARD 宏:

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

由于使用了 if constexpr 和推导出的 decltype(auto) 返回类型,我们的 IILE 有时可以返回引用,有时可以返回值,具体取决于检测到的类别。

这完美地保留了类型和值类别。请查看 Compiler Explorer 以获取实时演示。

英文:

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 &lt;typename T&gt;
constexpr auto category_of(std::remove_reference_t&lt;T&gt;&amp; t)
    -&gt; std::integral_constant&lt;value_category, value_category::lvalue&gt;;

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

} // 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(...) [&amp;]() -&gt; decltype(auto) { \
    constexpr auto c = decltype(::detail::category_of&lt;decltype(__VA_ARGS__)&gt;(__VA_ARGS__))::value; \
    if constexpr (c == ::detail::value_category::prvalue) { \
        return (__VA_ARGS__); \
    } \
    else { \
        return static_cast&lt;decltype(__VA_ARGS__)&amp;&amp;&gt;(__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.

huangapple
  • 本文由 发表于 2023年6月26日 23:25:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/76558118.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定