如何使转发引用参数仅绑定到右值引用?

50
我正在编写一个网络库,并且大量使用移动语义来处理文件描述符的所有权。我的其中一个类希望接收其他类型的文件描述符包装器并拥有所有权,所以大致是这样的。
struct OwnershipReceiver
{
  template <typename T>
  void receive_ownership(T&& t)
  {
     // taking file descriptor of t, and clear t
  }
};

它必须处理多个不相关的类型,所以receive_ownership()必须是一个模板。为了安全起见,我希望它只绑定到右值引用,这样用户在传递左值时必须明确使用std::move()

receive_ownership(std::move(some_lvalue));

但问题是,C++模板推导允许不需要额外努力就可以传递一个左值。而我曾经不小心将一个左值传递给receive_ownership(),并在之后使用了那个被清除的左值,结果自己踩了个坑。
所以问题来了:如何创建一个只绑定到右值引用的模板参数?
6个回答

50

您可以限制 T 不是一个左值引用,从而防止左值绑定到它:

#include <type_traits>

struct OwnershipReceiver
{
  template <typename T,
            class = typename std::enable_if
            <
                !std::is_lvalue_reference<T>::value
            >::type
           >
  void receive_ownership(T&& t)
  {
     // taking file descriptor of t, and clear t
  }
};

也许将 T 添加某种限制,使其仅接受文件描述符包装器,会是一个不错的主意。


9
你尝试过使用 is_rvalue_reference<T&&>::value 吗?(注意 && 符号) - fredoverflow
3
@fredoverflow,那么应该使用!std::is_lvalue_reference<T>::value还是std::is_rvalue_reference<T&&>::value?(第二种方法似乎更优雅,因为它告诉你t的实际类型。) - alfC
3
@alfC在https://dev59.com/KlQJ5IYBdhLWcg3wel5n中给出了一个很好的解释。 - bradgonesurfing
1
@solstice333:搜索“通用引用”和“完美转发”。 - Howard Hinnant
1
@solstice333 这样定义是为了允许完美转发。当然,他们本可以使用不同的语法(可能是个好主意),但他们说如果你想要一个绑定到左值或右值的类型,T&& a 就可以做到这一点。这也意味着如果我们想要一个只接受模板类型 T 的右值引用的模板,我们必须编写额外的代码。最终他们说你可以将其写为 auto&& a,但其他语法将永远存在。 - user904963
显示剩余3条评论

19
一种简单的方法是提供一个接受左值引用的“已删除成员”。
template<typename T> void receive_ownership(T&) = delete;

这将始终是一个更好的匹配,适用于左值参数。
如果您有一个需要多个参数的函数,而且所有参数都需要是右值(rvalue),那么我们将需要多个已删除的函数。在这种情况下,我们可能更倾向于使用SFINAE来隐藏函数,使其对任何左值参数都不可见。
一种实现方式是使用C++20的Concepts特性:
#include <type_traits>

template<typename T>
void receive_ownership(T&& t)
    requires !std::is_lvalue_reference<T>::value
{
     // taking file descriptor of t, and clear t
}

或者

#include <type_traits>

void receive_ownership(auto&& t)
    requires std::is_rvalue_reference<decltype(t)>::value
{
     // taking file descriptor of t, and clear t
}

稍微再深入一点,你可以定义一个全新的概念,这可能会很有用,如果你想要重复使用它,或者只是为了更清晰明了:
#include <type_traits>

template<typename T>
concept rvalue = std::is_rvalue_reference<T&&>::value;

void receive_ownership(rvalue auto&& t)
{
    // taking file descriptor of t, and clear t
}

只是为了完整起见,这是我的简单测试:
#include <utility>
int main()
{
    int a = 0;
    receive_ownership(a);       // error
    receive_ownership(std::move(a)); // okay

    const int b = 0;
    receive_ownership(b);       // error
    receive_ownership(std::move(b)); // allowed - but unwise
}

当我们传递std::move(b)时,我们得到的是一个对常量对象的右值引用,这很可能会导致复制。我们可以通过修改概念来禁止对常量的引用(我将重新命名以更好地反映意图)来防止这种情况:
template<typename T>
concept movable_from = std::is_rvalue_reference<T&&>::value
    && !std::is_const_v<std::remove_reference_t<T>>;

2
我不能使用 template <typename T> void receive_ownership(T& t) = delete 吗? - Xeverous
是的,那个方法可行且更简单 - 我已经相应地进行了编辑。谢谢你的提示。 - Toby Speight
但是那么它不应该是 const T& 或者两者都是吗? - Xeverous
我不这么认为,因为 T 既可以绑定到 const 类型,也可以绑定到非 const 类型。我实际上尝试了这个示例。 - Toby Speight
T& 可以绑定,但 T 无法绑定到 const Tconst T& 对象。 - Xeverous
你实际尝试过吗?从 receive_ownership(b); 的错误信息可以清楚地看出,T 被推断为 const int - Toby Speight

6
我发现有些人容易混淆的一个问题:使用SFINAE是可以的,但我不能使用以下内容:
std::is_rvalue_reference<T>::value

唯一能够按照我的要求工作的方式是:
!std::is_lvalue_reference<T>::value

原因是:我需要我的函数接收一个rvalue,而不是一个rvalue 引用。使用std::is_rvalue_reference<T>::value有条件地启用的函数将不会接收到rvalue,而是接收到rvalue引用。

2
对于左值引用,T被推断为左值引用,而对于右值引用,T被推断为非引用。因此,如果函数绑定到右值引用,编译器最终看到的某个类型T是:
std :: is_rvalue_reference :: value 而不是
std :: is_rvalue_reference :: value

1
很不幸,似乎尝试使用is_rvalue_reference<TF>(其中TF是完美转发的类型)并不能很好地区分const T&T&&(例如,在两者中使用enable_if,一个带有is_rvalue_reference_v<TF>,另一个带有!is_rvalue_reference_V<TF>)。
一种解决方案(虽然有点hacky)是将转发的T衰减,然后将这些重载放置在了一个知道这些类型的容器中。生成的this example
Hup,我错了,只是忘了看Toby的答案(is_rvalue_reference<TF&&>)--虽然可以使用std::forward<TF>(...),但这很令人困惑,不过我猜这就是为什么decltype(arg)也起作用的原因。
无论如何,以下是我用于调试的内容:(1)使用struct重载,(2)使用错误的检查is_rvalue_reference,以及(3)正确的检查。
/*
Output:

const T& (struct)
const T& (sfinae)
const T& (sfinae bad)
---
const T& (struct)
const T& (sfinae)
const T& (sfinae bad)
---
T&& (struct)
T&& (sfinae)
const T& (sfinae bad)
---
T&& (struct)
T&& (sfinae)
const T& (sfinae bad)
---
*/

#include <iostream>
#include <type_traits>

using namespace std;

struct Value {};

template <typename T>
struct greedy_struct {
  static void run(const T&) {
    cout << "const T& (struct)" << endl;
  }
  static void run(T&&) {
    cout << "T&& (struct)" << endl;
  }
};

// Per Toby's answer.
template <typename T>
void greedy_sfinae(const T&) {
  cout << "const T& (sfinae)" << endl;
}

template <
    typename T,
    typename = std::enable_if_t<std::is_rvalue_reference<T&&>::value>>
void greedy_sfinae(T&&) {
  cout << "T&& (sfinae)" << endl;
}

// Bad.
template <typename T>
void greedy_sfinae_bad(const T&) {
  cout << "const T& (sfinae bad)" << endl;
}

template <
    typename T,
    typename = std::enable_if_t<std::is_rvalue_reference<T>::value>>
void greedy_sfinae_bad(T&&) {
  cout << "T&& (sfinae bad)" << endl;
}

template <typename TF>
void greedy(TF&& value) {
  using T = std::decay_t<TF>;
  greedy_struct<T>::run(std::forward<TF>(value));
  greedy_sfinae(std::forward<TF>(value));
  greedy_sfinae_bad(std::forward<TF>(value));
  cout << "---" << endl;
}

int main() {
  Value x;
  const Value y;

  greedy(x);
  greedy(y);
  greedy(Value{});
  greedy(std::move(x));

  return 0;
}

-1

使用更现代的C++,我们可以简单地require T&& 是一个右值引用:

#include <type_traits>

template<typename T> requires std::is_rvalue_reference_v<T&&>
void receive_ownership(T&&)
{
    // taking file descriptor of t, and clear t
}

简单演示:

#include <string>
#include <utility>
int main()
{
    auto a = std::string{};
    auto const b = a;

    receive_ownership(a);            // ERROR
    receive_ownership(std::move(a)); // okay

    receive_ownership(b);            // ERROR
    receive_ownership(std::move(b)); // okay - but unwise!

    receive_ownership(std::string{}); // okay
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接