Why would you explicitly move a forwarding reference?
Asked Answered
C

3

16

I'm looking at some code, and I see the following function:

template <typename... Args>
static return_t make_return(Args &&... args)
{
    // using std::forward<Args> will preserve lvalue args as such, but the point of this function
    // is to make a return, where the 99.9+% case is moving a local (lvalue) into the return pack.
    // Thus it forces a move, which will move `T&` args (but _not_ `const T&` args) into the
    // return pack so that users don't need to type out a bunch of std::moves themselves/
    // If they don't want implicit move they can just call std::make_tuple directly
    return std::make_tuple(std::move(args)...);
}

The documentation here confuses me.
Why would you explicitly move a forwarding reference?
Wouldn't you want to preserve the lvalue / rvalue within a generic context?
I'm having trouble understanding the rationale or how this behavior would be different from the recommended std::forward.

To put it another way,
I've never seen anyone explicitly opt-out from perfectly-forwarding a forwarding reference.
Does it make sense?

Corvese answered 6/2, 2018 at 19:32 Comment(3)
Ugh. Sometimes C++'s ugly shows so much that I wonder why I even try to understand it.Ventre
@MichaelDorgan The question is "why would you use && other than for logical and."Cecrops
Imagine trying to implement std::move, would you use std::forward in it? This make_return is similar to std::move (maybe the name of the function could have been more explicit).Decanter
H
5

A small example reveals the author's intent:

#include <tuple>
#include <iostream>

struct A {
  A() { }
  A(const A&) { std::cout << "copy\n"; }
  A(A&&) { std::cout << "move\n"; }
};

template <typename Arg>
static std::tuple<A> make_return(Arg&& arg) {
  return std::make_tuple(std::move(arg));
}

void f(const std::tuple<A>&) { }

void f1() {
  std::cout << "return local via make_tuple: ";
  A a{};
  f(std::make_tuple(a));
}

void f2() {
  std::cout << "return local via make_tuple(move): ";
  A a{};
  f(std::make_tuple(std::move(a)));
}

void f3() {
  std::cout << "return local via make_return: ";
  A a{};
  f(make_return(a));
}

void f4() {
  std::cout << "return const via make_tuple: ";
  const A a{};
  f(std::make_tuple(a));
}

void f5() {
  std::cout << "return const via make_tuple(move): ";
  const A a{};
  f(std::make_tuple(std::move(a)));
}

void f6() {
  std::cout << "return const via make_return: ";
  const A a{};
  f(make_return(a));
}

int main() {
  f1();
  f2();
  f3();
  f4();
  f5();
  f6();
}

Output:

return local via make_tuple: copy
return local via make_tuple(move): move
return local via make_return: move
return const via make_tuple: copy
return const via make_tuple(move): copy
return const via make_return: copy

In cases where a local non-const variable is returned, we want to std::move its contents. This is achievable using std::make_tuple(std::move(a)), because a plain std::make_tuple(a) would copy. To save some typing, the author wrote make_return as a shorthand for std::make_tuple(std::move(a)): the example shows that f3 works just like f2.

When a constant is passed, std::move won't make any difference, but no harm either. So we could use std::make_tuple, but make_return works just fine, too. Cases f4, f5, f6 all behave the same, showing that one doesn't really need to think twice before mixing constants and non-constants in make_return (in the case of multiple entries constituting return_t).

What remains is moving a non-const variable that is not local to the function, and thus we wouldn't like to destroy its contents. In these cases make_return is unwanted and one would need to resort back to manual invocation of std::make_tuple (utilizing std::move where appropriate only).

Now what would this look like with std::forward? Changing the definition of make_return to utilizing

std::make_tuple(std::forward<Arg>(arg));

produces:

return local via tuple: copy
return local via tuple(move): move
return local via make_return: copy
return const via tuple: copy
return const via tuple(move): copy
return const via make_return: copy

since a in f3 gets passed as a const A&. Indeed, make_return is then, by the logic of forwarding, a mere synonyme for std::move, losing any benefit we hoped to achieve.

Huron answered 6/2, 2018 at 20:12 Comment(0)
S
6

Why would you explicitly move a forwarding reference?

Because it isn't being used for its forwarding properties. Yes, you are correct, we usually std::forward a forwarding reference. But in this case the author used forwarding references solely as a matter of convenience. If it was written as make_return(Args&... args) then it would be impossible to pass an rvalue to make_return, since a non-const lvalue reference may not bind to one.

By using forwarding references, the author allows passing values of any value category into the function, without incurring extra copies if none are needed. The documentation is there to clarify that the function signature isn't for forwarding, but simply for binding to whatever arguments it's given to move out.

Semipalmate answered 6/2, 2018 at 20:1 Comment(0)
H
5

A small example reveals the author's intent:

#include <tuple>
#include <iostream>

struct A {
  A() { }
  A(const A&) { std::cout << "copy\n"; }
  A(A&&) { std::cout << "move\n"; }
};

template <typename Arg>
static std::tuple<A> make_return(Arg&& arg) {
  return std::make_tuple(std::move(arg));
}

void f(const std::tuple<A>&) { }

void f1() {
  std::cout << "return local via make_tuple: ";
  A a{};
  f(std::make_tuple(a));
}

void f2() {
  std::cout << "return local via make_tuple(move): ";
  A a{};
  f(std::make_tuple(std::move(a)));
}

void f3() {
  std::cout << "return local via make_return: ";
  A a{};
  f(make_return(a));
}

void f4() {
  std::cout << "return const via make_tuple: ";
  const A a{};
  f(std::make_tuple(a));
}

void f5() {
  std::cout << "return const via make_tuple(move): ";
  const A a{};
  f(std::make_tuple(std::move(a)));
}

void f6() {
  std::cout << "return const via make_return: ";
  const A a{};
  f(make_return(a));
}

int main() {
  f1();
  f2();
  f3();
  f4();
  f5();
  f6();
}

Output:

return local via make_tuple: copy
return local via make_tuple(move): move
return local via make_return: move
return const via make_tuple: copy
return const via make_tuple(move): copy
return const via make_return: copy

In cases where a local non-const variable is returned, we want to std::move its contents. This is achievable using std::make_tuple(std::move(a)), because a plain std::make_tuple(a) would copy. To save some typing, the author wrote make_return as a shorthand for std::make_tuple(std::move(a)): the example shows that f3 works just like f2.

When a constant is passed, std::move won't make any difference, but no harm either. So we could use std::make_tuple, but make_return works just fine, too. Cases f4, f5, f6 all behave the same, showing that one doesn't really need to think twice before mixing constants and non-constants in make_return (in the case of multiple entries constituting return_t).

What remains is moving a non-const variable that is not local to the function, and thus we wouldn't like to destroy its contents. In these cases make_return is unwanted and one would need to resort back to manual invocation of std::make_tuple (utilizing std::move where appropriate only).

Now what would this look like with std::forward? Changing the definition of make_return to utilizing

std::make_tuple(std::forward<Arg>(arg));

produces:

return local via tuple: copy
return local via tuple(move): move
return local via make_return: copy
return const via tuple: copy
return const via tuple(move): copy
return const via make_return: copy

since a in f3 gets passed as a const A&. Indeed, make_return is then, by the logic of forwarding, a mere synonyme for std::move, losing any benefit we hoped to achieve.

Huron answered 6/2, 2018 at 20:12 Comment(0)
S
0

make_return() is to return a tuple using a value, since this value won't be needed anymore, as is used in make_return (end scope of the a function), there's no need to use std::forward<>, since it could forward lvalue references which incurs in copies (depending on implementation), but value is at the end of scope so is not needed to preserve any resources.

Forcing std::move on the make_tuple, forces to use rvalue references first, omitting possible more overhead (depending on implementation).

Speculative answered 6/2, 2018 at 20:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.