C++ 模板函数中的左值和右值

C++ lvalues and rvalues in template functions

我从 http://www.cplusplus.com/reference/utility/forward:

中抽取样本
// forward example
#include <utility>      // std::forward
#include <iostream>     // std::cout

// function with lvalue and rvalue reference overloads:
void overloaded (const int& x) {std::cout << "[lvalue]";}
void overloaded (int&& x) {std::cout << "[rvalue]";}

// function template taking rvalue reference to deduced type:
template <class T> void fn (T&& x) {
    overloaded (x);                   // always an lvalue
    overloaded (std::forward<T>(x));  // rvalue if argument is rvalue
}

int main () {
    int a;

    std::cout << "calling fn with lvalue: ";
    fn (a);
    std::cout << '\n';

    std::cout << "calling fn with rvalue: ";
    fn (0);
    std::cout << '\n';

    return 0;
}

打印

calling fn with lvalue: [lvalue][lvalue]
calling fn with rvalue: [lvalue][rvalue]

但是如果我想制作 重载 模板,就像这样:

template<typename T>
void overloaded (const T& x) {std::cout << "[lvalue]";}
template<typename T>
void overloaded (T&& x) {std::cout << "[rvalue]";}

它打印

calling fn with lvalue: [rvalue][rvalue]
calling fn with rvalue: [rvalue][rvalue]

有什么方法可以使用模板函数来推导我传递给函数的对象是什么?

是的,这是可能的,只需将 const 添加到您的第二个重载:

template<typename T>
void overloaded (const T& x);
template<typename T>
void overloaded (const T&& x);
//               ^^^^^

之所以需要 const 是为了使 x 不是转发参考。转发引用非常贪婪,如果您不传递完全相同的类型(包括任何 cv 限定符),那么将选择转发引用重载。

在你的情况下,因为你没有将 const 对象传递给 overload,所以第二个重载总是更好的匹配。

但是如果你在那里添加一个 const ,那么它就不再是转发引用并且只能接受右值而不接受左值,因此不会更好地匹配左值但会比 const& 重载更好地匹配任何右值。


如果您需要从 x 移动,那么您将不得不做其他事情。删除转发引用中的 const& 重载和分支,无论您有右值还是左值:

template <typename T> void overloaded(T &&x) {
  if /*constexpr*/ (std::is_lvalue_reference_v<T>)
    std::cout << "[lvalue]";
  else
    std::cout << "[rvalue]";
}

注意:如果您执行对某个分支或其他分支无效的特定操作,则需要使用 if constexpr

只要函数模板参数的类型为 "rvalue reference to a type template parameter",就像 template<typename T> void overloaded(T&& x); 中的 x,它就会变成 "forwarding reference" 又名 "universal reference"。它们遵循特殊规则,即它们可以匹配左值或右值参数。

当参数为右值时,T其类型为非引用,参数类型为右值引用。

当参数为左值时,T为左值引用类型。这是有道理的,因为第二个 "reference-collapsing rule":如果您将 &&& 添加到类型别名(typedef,用 using 定义的类型,或者类型模板参数)已经是一个引用,它形成一个引用类型,如果别名都是右值引用并且您添加 && 或在其他三种情况下为左值引用,则它是右值引用。

所以当你调用 overloaded(x)x 是类型 int 的左值,overloaded 的两个重载匹配:

template<typename T>
void overloaded (const T& x);
// Specialization void overloaded<int>(const int&);

template<typename T>
void overloaded (T&& x);
// Specialization void overloaded<int&>(int&);

但是第二个特化更适合参数类型,所以它获胜。

为了防止这种情况,我会使用 SFINAE 技术将第二个 overloaded 限制为仅非引用类型。

#include <type_traits>

// As before:
template<typename T>
void overloaded (const T& x);

template<typename T>
auto overloaded (T&& x) -> std::enable_if_t<!std::is_reference<T>::value>;

现在给定任何左值参数,第二个模板将推导出 T 为左值引用类型,无法将其替换为 return 类型,然后模板将因重载而被忽略分辨率,以便可以改用第一个重载。

第二个 overloaded 模板不适用于右值,而是转发引用。它倾向于非常积极地接听电话,因为它会准确地推断出 const-enss 和 value-ness,使其在几乎所有情况下都是首选(您的 int 调用带有左值但它不是 const,所以这就是它被调用的原因).

要完成这项工作,您需要限制该模板。当该模板匹配左值时,T 将是引用类型,因此引用折叠规则表明 T&& 将折叠为 T&。所以我们可以这样做:

template<typename T>
void overloaded (const T& x) {std::cout << "[lvalue]";}

template<typename T, std::enable_if_t<!std::is_reference<T>::value, int> = 0>
void overloaded (T&& x) {std::cout << "[rvalue]";}

如果你这样做,那么你应该得到预期的输出。