问题
I'm writing a network library and use move semantics heavily to handle ownership for file descriptors. One of my class wishes to receive file descriptor wrappers of other kinds and take ownership, so it's something like
struct OwnershipReceiver
{
template <typename T>
void receive_ownership(T&& t)
{
// taking file descriptor of t, and clear t
}
};
It has to deal multiple unrelated types so receive_ownership has to be a template, and to be safe, I wish it ONLY binds to rvalue references, so that user has to explicitly state std::move when passing an lvalue.
receive_ownership(std::move(some_lvalue));
But the problem is: C++ template deduction allows an lvalue to be passed in without extra effort. And I actually shot myself on the foot once by accidentally passing an lvalue to receive_ownership and use that lvalue(cleared) later.
So here is the question: how to make a template ONLY bind to rvalue reference?
回答1:
You can restrict T
to not be an lvalue reference, and thus prevent lvalues from binding to it:
#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
}
};
It might also be a good idea to add some sort of restriction to T
such that it only accepts file descriptor wrappers.
回答2:
A simple way is to provide a deleted member which accepts an lvalue reference:
template<typename T> void receive_ownership(T&) = delete;
This will always be a better match for an lvalue argument.
If you have a function that takes several arguments, all of which need to be rvalues, we will need several deleted functions. In this situation, we may prefer to use SFINAE to hide the function from any lvalue arguments.
One way to do this could be with C++17 and the Concepts TS:
#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
}
or
#include <type_traits>
void receive_ownership(auto&& t)
requires std::is_rvalue_reference<decltype(t)>::value
{
// taking file descriptor of t, and clear t
}
Going slightly further, you're able to define a new concept of your own, which may be useful if you want to reuse it, or just for extra clarity:
#include <type_traits>
template<typename T>
concept bool rvalue = std::is_rvalue_reference<T&&>::value;
void receive_ownership(rvalue&& t)
{
// taking file descriptor of t, and clear t
}
Note: with GCC 6.1, you'll need to pass -fconcepts
to the compiler, as it's an extension to C++17 rather than a core part of it.
Just for completeness, here's my simple test:
#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
}
回答3:
I learnt something that seems to confuse people quite often: using SFINAE is OK, but I can't use:
std::is_rvalue_reference<T>::value
The only way it works as I want is
!std::is_lvalue_reference<T>::value
The reason is: I need my function to receive an rvalue, not an rvalue reference. A function conditionally enabled with std::is_rvalue_reference<T>::value
will not receive an rvalue, but rather receives an rvalue reference.
回答4:
For lvalue references, T is deduced to be an lvalue reference, and for rvalue references, T is deduced to be a non-reference.
So if the function binds to a rvalue reference, what is seen at the end by the compiler for a certain type T is:
std::is_rvalue_reference<T>::value
and not
std::is_rvalue_reference<T&&>::value
回答5:
Unfortunately, it seems like trying out is_rvalue_reference<TF>
(where TF
is the perfectly-forwarded type) does not work well if you are actually trying to make overloads that distinguish between const T&
and T&&
(e.g. using enable_if
in both, one with is_rvalue_reference_v<TF>
and the other with !is_rvalue_reference_V<TF>
).
A solution (albeit hacky) is to decay the forwarded T
, then place the overloads in a container aware of these types. Generated this example:
Hup, I was wrong, just forgot to look at Toby's answer (is_rvalue_reference<TF&&>
) -- though it's confusing that you can do std::forward<TF>(...)
, but I guess that's why decltype(arg)
also works.
Anywho, here's what I used for debugging: (1) using struct
overloads, (2) using the wrong check for is_rvalue_reference
, and (3) the correct check:
/*
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;
}
来源:https://stackoverflow.com/questions/7863603/how-to-make-template-rvalue-reference-parameter-only-bind-to-rvalue-reference