How can I make a forwarding-reference parameter bind only to rvalue references?
Asked Answered
E

5

51

I'm writing a network library and use move semantics heavily to handle ownership for file descriptors. One of my classes 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 with multiple unrelated types so receive_ownership() has to be a template. To be safe, I wish it to bind only to rvalue references, so that user has to explicitly use std::move() when passing an lvalue.

receive_ownership(std::move(some_lvalue));

But the problem is that C++ template deduction allows an lvalue to be passed in without extra effort. And I actually shot myself in the foot once by accidentally passing an lvalue to receive_ownership() and using that lvalue (cleared) later.

So here is the question: how to make a template parameter that binds only to rvalue references?

Excavate answered 23/10, 2011 at 0:42 Comment(0)
M
51

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.

Morass answered 23/10, 2011 at 0:51 Comment(8)
You could use std::is_rvalue_reference insteadAlfrediaalfredo
Thanks Howard, that works fine. And Dave, I thought exactly the same as you do at the beginning, then I found std::is_rvalue_reference is not gonna work: it doesn't bind to a "real" rvalue, or even std::move()ed lvalue.Excavate
@Ralph: Did you try is_rvalue_reference<T&&>::value? (Note the &&)Gerhardine
@fredoverflow, so, what should one use !std::is_lvalue_reference<T>::value or std::is_rvalue_reference<T&&>::value? (the second seems more elegant because it tells you wat t really is.Ona
@Ona A good explanation is now given at #53759296Neoma
any explanation as to why an lvalue can be bound to rvalue ref like that when it comes to template deduction? I don't really understand that part.Sonics
@solstice333: Search for "universal reference" and "perfect forwarding".Morass
@Sonics It's defined that way to allow for perfect forwarding. Of course, they could have made different syntax (probably would have been a good idea), but instead, they said if you want a type that binds to either an lvalue or rvalue, T&& a will do that. This also means if we want a template that takes only an rvalue reference of template type T, we have to write extra code. They eventually said you can write that as auto&& a, but the other syntax is there forever.Heptangular
S
23

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++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
}

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 rvalue = std::is_rvalue_reference<T&&>::value;

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

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
}

When we pass std::move(b) we have an rvalue reference to a const object, which will likely result in a copy. We can prevent that by modifying the concept to disallow references to const (I'll rename it to better reflect the intent):

template<typename T>
concept movable_from = std::is_rvalue_reference<T&&>::value
    && !std::is_const_v<std::remove_reference_t<T>>;
Scissile answered 3/10, 2016 at 9:7 Comment(6)
Can't I template <typename T> void receive_ownership(T& t) = delete?Glynda
Yes, that works and is simpler - I've edited accordingly. Thank you for the hint.Scissile
But then shouldn't it be const T& or both?Glynda
I don't think so, because T will bind to const as well as to non-const types. I did actually try the example.Scissile
T would bind but it's T&. It will not bind to const T or const T& objectsGlynda
Have you actually tried? The error message from receive_ownership(b); clearly shows that T is deduced as const int.Scissile
E
6

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.

Excavate answered 23/10, 2011 at 12:13 Comment(0)
M
2

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

Manteltree answered 30/9, 2016 at 14:30 Comment(0)
C
1

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;
}
Cenac answered 4/1, 2018 at 21:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.