C++ pass parameter by rvalue reference if possible, otherwise copy the lvalue reference
Asked Answered
U

4

6

With rvalue references, many redundant copies may be elided, but that seems to require me to write the same function multiple times (one for an rvalue reference, one for a const lvalue reference). But the standard library seems to only need to declare some functions once.

For example:

#include <iostream>
#include <tuple>

void foo(int&& x){
    x = 2;
}

int main()
{
    int x = 1;
    foo(x); // compile error
    std::make_tuple(x); // ok
    std::cout << x << std::endl;
}

Calling foo(x) is a compile error, because I cannot convert implicitly from int to int&&. But I am perplexed as to why std::make_tuple would work. The reference says that it only accepts rvalue reference parameters. It also seems to not make copies when the value passed into it is an ravlue reference, but it would make a copy (as most would expect) when used as in my sample above.

How can I make foo work like this?

Uriel answered 23/3, 2017 at 13:36 Comment(0)
D
8

The reference says that it only accepts rvalue reference parameters.

No, this is forwarding reference, which could serve as both lvalue reference and rvalue reference, according to the value category of the passed-in argument.

How can I make foo work like this?

The point of declaring a forwarding reference is (1) type deduction is necessary, that means you need to make foo a function template here; (2) the parameter x has the exact form of T&& for the template parameter T. e.g.

template <typename T>
void foo(T&& x){
    x = 2;
}

then

int x = 1;
foo(x);   // lvalue passed, T is deduced as int&, parameter's type is int&
foo(1);   // rvalue passed, T is deduced as int, parameter's type is int&&

Note this is true for std::make_tuple too, even it's using template parameter pack. And better to bear in mind that even forwarding reference looks like rvalue reference but they're different things.

BTW: std::forward is usually used with forwarding reference to preserve the value category of the argument e.g. when forwarding it to other functions.

Diminution answered 23/3, 2017 at 13:41 Comment(6)
Thanks for the elaboration. If instead of an int, foo needs to accept a std::string (or some reference of it), is it good to make foo a template for this forwarding behaviour? Anything else not assignable to a std::string will not make sense, and should not be passed into foo.Uriel
@Uriel In those cases you use std::enable_if to constraint T to being a std::string.Elder
@Uriel It depends on how you use the parameter inside foo; if something wrong passed to foo you might get a compilation error. Anyway, forwarding references are designed to be able to accept almost any type of arguments; so if foo only works with std::string, forwarding references might be over-kill. On the other hand, if you need a more general type, e.g. not only std::string, also string literals (const char[]), then forwarding reference would be appropriate.Diminution
@Diminution That's a good point, it might be better to accept anything assignable to a std::string, including a std::string_view (C++17).Uriel
@Uriel Yes. Depends on your usage, not only copy may be omitted, but also move; the argument could be used directly, no need to construct a (temporary) std::string every time.Diminution
@Diminution Could you give me an example? I can't think of any code that will omit move, but will require a copy if the argument is an lvalue reference.Uriel
I
4

The difference is a result of a rather subtle overload of the && operator when it is used with a template parameter:

template<typename foo>
void bar(foo &&baz)

This is not an rvalue reference. It is a forwarding reference. When the template gets resolved, baz is going to be either an lvalue or an rvalue reference, depending on the call situation.

This is why you will see the C++ library provide what appears to be a single template, that works in both lvalue and rvalue contexts.

But forwarding references occur in templates only. In a non-template declaration:

void bar(int &&baz)

This is always an rvalue reference, and can only be used in rvalue contexts.

Isobar answered 23/3, 2017 at 13:41 Comment(0)
E
3

std::make_tuple works because the function doesn't actually take a rvalue reference like foo does. std::make_tuple takes a object in the form of T&& and since T is a template type it makes it a forwarding reference, not a rvalue reference. A forwarding reference can bind to lvalues and rvalues where as a rvalue reference can only take a rvalue. To make foo the same you would need

template<typename T>
void foo(T&& bar)
{
    bar = 2;
}
Elder answered 23/3, 2017 at 13:41 Comment(0)
E
2

Would this work for you?

#include <iostream>
#include <tuple>

void foo(int&& x){
    std::cout<<"rvalue"<<std::endl;
    x = 2;
}

void foo(int& x){
    std::cout<<"lvalue"<<std::endl;
    x = 2;
}

int main()
{
    int x = 1;
    foo(x); // no compile error anymore
    foo(std::move(x)); // now r-value is being used        
    std::make_tuple(x); // ok
    std::cout << x << std::endl;
}
Electioneer answered 23/3, 2017 at 13:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.