当函数接受 T&& 的参数并传递左值时,模板参数 T 是否应该解析为 T&?

Should template parameter T be resolving to T& when a function takes argument of T&& and is passed an lvalue?

我正在尝试在 C++ 中实现完美转发,因此我编写了以下快速而肮脏的代码来测试它。

class CValue
{
public:

    CValue(int i) :
        m_i(i)
    {
        printf("Default constructor called!\r\n");
    }

    CValue(const CValue& src) :
        m_i(src.m_i)
    {
        printf("Copy constructor called!\r\n");
    }

    CValue(CValue&& src) :
        m_i(src.m_i)
    {
        printf("Move constructor called!\r\n");
    }

    CValue& operator=(const CValue& src)
    {
        m_i = src.m_i;
        printf("Copy assignment called!\r\n");
        return *this;
    }

    CValue& operator=(CValue&& src)
    {
        m_i = src.m_i;
        printf("Move assignment called!\r\n");
        return *this;
    }

    int     m_i;
};

template <typename T>
void PerfectForwarding(T&& tValue)
{
    T tValue1 = tValue;
    tValue1.m_i = 10;
}

int _tmain(int argc, _TCHAR* argv[])
{
    CValue v(0);
    PerfectForwarding(v);

    printf("%d\r\n", v.m_i);

    return 0;
}

当我构建并运行此代码作为控制台应用程序时,我得到了答案 10,而我期望的是 0。

这行好像是:

T tValue1 = tValue;

在 PerfectForwarding 函数中被解析为:

CValue& tValue1 = tValue;

而不是:

CValue tValue1 = tValue;

所以编译器将 T 解析为 CValue&,这是我没想到的。

我尝试通过显式声明模板参数类型从 _tmain 调用函数,即

PerfectForwarding<CValue>(v);

但是编译失败并出现以下错误:

error C2664: 'void PerfectForwarding<CValue>(T &&)' : cannot convert
              argument 1 from 'CValue' to 'CValue &&'
              with
              [
                  T=CValue
              ]
You cannot bind an lvalue to an rvalue reference

我可以通过将 PerfectForwarding 函数中的行更改为以下内容来强制执行所需的行为:

typename std::remove_reference<T>::type tValue1 = tValue;

但我不认为这是必要的。根据引用折叠规则,参数类型 (T&&) 应该变成 CValue&(如 T&& & -> T&),但是 T 本身应该只是 CValue,对吧?这是 VC12 编译器处理右值引用的错误,还是我误解了右值引用和模板?

我在调试中使用 Visual Studio 2013(VC12 编译器)并关闭所有优化。

行为正确。当参数是左值时,通过推导类型的左值引用来实现完美转发。

你是说

By the reference collapsing rules the type of the argument (T&&) should become CValue& (as T&& & -> T&), but T itself should be simply CValue, surely?

但是,如果 T 只是 CValueT&& & 中的 & 从何而来? T&& 作为转发引用的全部原因是 T 成为左值引用,在你的例子中是 CValue&,然后引用折叠得到 T && -> CValue & && -> CValue &.

标准的相关部分是C++11 14.8.2.1/3(P是函数模板参数类型,A是调用中参数的类型,定义在 14.8.2.1/1):

If P is a cv-qualified type, the top level cv-qualifiers of P’s type are ignored for type deduction. If P is a reference type, the type referred to by P is used for type deduction. If P is an rvalue reference to a cv-unqualified template parameter and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.

(强调我的)

当编译器只看到 T&& 时,它会尝试将 T 推断为允许使用您提供的任何内容调用该函数的东西。因此,当使用左值调用时,您最终 T 成为 Cvalue& ,因此引用崩溃(正如您所指出的那样)可以开始。

当你试图转发这个论点时,这很重要。 std::forward<T>(v) 将参数转发为左值或右值,具体取决于 T 是什么。如果它被推导为左值引用,它将作为左值转发,如果没有,它将作为右值转发。类型 T 是唯一的区别。

根据引用折叠规则,参数类型 (T&&) 应该变成 CValue&(如 T&& & -> T&),但 T 本身应该只是 CValue,对吗?
如果使用左值调用,它将 (T&& v) "okay, I can't bind an rvalue reference to an lvalue, but if I make T itself be an lvalue reference then this works." T 推导为 Cvalue&,以便 (T&& v) 扩展为 (Cvalue& && v)。现在引用已折叠 (Cvalue& v)。类型 T 必须是左值引用类型才能工作。

如果您明确提供模板参数,那么您并没有真正解决问题。解决此问题的第一种方法是您发现的 remove_reference 。您还可以使用 auto 这更有意义,因为这是通用编程

auto tValue1 = tValue;

在任何情况下,auto 都不会推导出引用。对于您提供的内容,您最好使用可以绑定到左值和右值的 const 左值引用。

template <typename T>
void PerfectForwarding(const T& tValue)
{
    T tValue1 = tValue;
    tValue1.m_i = 10;
}

这不允许转发,但无论如何它都不是转发功能。

如果你想以不同的方式处理左值,你可以提供两个重载

template <typename T>
void PerfectForwarding(T& tValue);

template <typename T>
void PerfectForwarding(T&& tValue);

当使用左值调用时,前者将是首选。

编译器做了正确的事情。在像

这样的行中
template<typename T>
void f(T&& param){}
如果传递左值,

T 将被推断为引用,如果传递右值,则将推断为简单类型。所以 int x; f(x);T 推导为 int&,而 f(1)T 推导为 int.

这就是完美转发的工作原理。基本上,您有以下参考折叠规则:

& &   -> &
& &&  -> &
&& &  -> &
&& && -> &&

另见

Syntax for universal references

了解更多详情。