我正在编写一个网络库,并大量使用移动语义来处理文件描述符的所有权。我的一个类希望接收其他类型的文件描述符包装器并取得所有权,所以它就像
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()
并在稍后使用该左值(清除)。
所以问题是:如何制作一个将 only 绑定到右值引用的模板参数?
您可以限制
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
添加某种限制,使其仅接受文件描述符包装器也可能是个好主意。
一个简单的方法是提供一个接受左值引用的已删除成员:
template<typename T> void receive_ownership(T&) = delete;
这始终是左值参数的更好匹配。
如果您有一个带有多个参数的函数,并且所有参数都需要是右值,那么我们将需要几个已删除的函数。在这种情况下,我们可能更喜欢使用 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)
时,我们有一个对 const 对象的右值引用,这可能会产生一个副本。我们可以通过修改概念以禁止引用 const 来防止这种情况(我将重命名它以更好地反映意图):
template<typename T>
concept movable_from = std::is_rvalue_reference<T&&>::value
&& !std::is_const_v<std::remove_reference_t<T>>;
我学到了一些似乎经常让人们感到困惑的东西:使用 SFINAE 可以,但我不能使用:
std::is_rvalue_reference<T>::value
它按照我想要的方式工作的唯一方法是
!std::is_lvalue_reference<T>::value
原因是:我需要我的函数接收rvalue,而不是rvaluereference。使用
std::is_rvalue_reference<T>::value
有条件启用的函数不会接收右值,而是接收右值引用。
对于左值引用,T 被推导为左值引用,对于右值引用,T 被推导为非引用。
因此,如果函数绑定到右值引用,则编译器对于特定类型 T 最后看到的是:
std::is_rvalue_reference<T>::value
而不是
std::is_rvalue_reference<T&&>::value
不幸的是,如果您实际上试图进行区分
is_rvalue_reference<TF>
TF
的重载(例如使用 ),那么尝试
const T&
(其中
T&&
是完美转发的类型)似乎效果不佳两者都有 enable_if
,一个带有 is_rvalue_reference_v<TF>
,另一个带有 !is_rvalue_reference_V<TF>
)。一个解决方案(尽管很hacky)是衰减转发的
T
哈普,我错了,只是忘了看托比的答案(
is_rvalue_reference<TF&&>
)——虽然你可以做std::forward<TF>(...)
,这很令人困惑,但我想这就是为什么decltype(arg)
也有效。
Anywho,这是我用于调试的内容:(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;
}