Search code examples
c++perfect-forwardinglvalue-to-rvalue

How to use forwarding to cast rvalue to lvalue reference?


I'm trying to use an overloaded function to hide passing an int reference to a utility function when I don't care for the returned int data. Why is it that I need to use T& instead of T as the template argument? And why do I need the intermediate function that defines i as an rvalue?

class DataClass
{

};

template <typename T>
bool func(T& data, int& i) {
  i = 1;
  cout << "it worked!" << endl;
}

// I would like to remove this function
template <typename T>
bool func(T& data, int&& i) {
  return func(std::forward<T&>(data), std::forward<int&>(i));
}

template <typename T>
bool func(T& data) {
  // Why doesn't this line work?
  // return func(std::forward<T>(data), std::forward<int>(0));
  return func(std::forward<T&>(data), 0);
}

int main(int argc, char ** argv)
{
  DataClass d;
  func<DataClass>(d);
  return 0;
}

Solution

  • You don't need std::forward at all here. Except for int&& i in the second overload, your parameters all declared as non-const lvalue references, so you can't pass an rvalue to any of them. And in the int&& overload, if you want to call the lvalue function from the rvalue function, you can just name the parameter i, because a name is always an lvalue.

    template <typename T>
    bool func(T& data, int& i) {
      i = 1;
      cout << "it worked!" << endl;
    }
    
    template <typename T>
    bool func(T& data, int&& i) {
      return func(data, i);
    }
    
    template <typename T>
    bool func(T& data) {
      return func(data, 0);
    }
    

    If you'd like to remove a function, note that it's actually the int& that does something effectively different: it changes i to 1. The rvalue overload technically also does, but something that has been passed as an rvalue should generally be ignored after that point, so callers should only count on func(T&, int&&) to print the message to cout. And to take an int that isn't an lvalue... just take an int.

    template <typename T>
    bool func(T& data, int& i) {
      i = 1;
      cout << "it worked!" << endl;
    }
    
    template <typename T>
    bool func(T& data, int i=0) {
      return func(data, i); // effect on i is ignored.
    }
    
    // Okay to pass a const int?
    template <typename T>
    bool func(T&, const volatile int&&) = delete;
    

    That third deleted template preserves one behavior of your original code, though it's not clear if you actually want that behavior or not. In the function

    void test() {
        DataClass d;
        const int n = 5;
        func(d, n);
    }
    

    ... the original code would have failed to compile, since a const int lvalue can't bind to either int& or int&&. But the change to a parameter of simply int would allow this test to compile, by making a copy of n to be the plain int parameter. Then the change to that int i just gets discarded, even though you gave n to the function. The deleted template is a better match for the const int lvalue n, so it would cause test to fail to compile. If you do want the behavior where func(d, n) is valid but has no effect on n, just take out that deleted template.