1

I know there are cases where using std::forward on the same argument in a loop is wrong because it may cause moving from a moved object, like so:

template <typename T>
auto applyTenTimes(T&& arg, auto&& f){
    for(int i = 0; i < 10; ++i)
        f(std::forward<T>(arg));

    return arg; 
}

But, what about the case where the forwarded object gets assigned again? Like in this example:

template <typename T>
auto applyTenTimes(T&& arg, auto&& f){
    for(int i = 0; i < 10; ++i)
        arg = f(std::forward<T>(arg));

    return arg; 
}

Would this be valid? If yes, then why? Is it basically never creating a new object (when called with an rvalue) and just moving the arg into the function f and then gets moved back again into arg by RVO?

I tried looking at different StackOverflow questions, but none seemed to have what I was looking for!

15
  • 1
    Function parameters are not subjects of RVO: Why is RVO disallowed when returning a parameter? Commented Sep 30, 2023 at 19:16
  • The spamming tags is a bad idea, especially when C++11 has nothing todo with mandatory RVO. Commented Sep 30, 2023 at 19:19
  • @273K ahh, I didn't know that. Does that affect the question though? Commented Sep 30, 2023 at 19:20
  • I thought std::forward was introduced in C++11. Commented Sep 30, 2023 at 19:21
  • 1
    @IWonderWhatThisAPIDoes that template will not compile for const arguments. Commented Sep 30, 2023 at 19:32

3 Answers 3

2

std::forward, this will expand according to the input being an rvalue, or lvalue as follows.

// for lvalue or moved lvalue
template <typename T>
auto applyTenTimes(T& arg, auto&& f){
    for(int i = 0; i < 10; ++i)
        arg = f(arg);

    return arg; 
}

// for rvalue
template <typename T>
auto applyTenTimes(T arg, auto&& f){
    for(int i = 0; i < 10; ++i)
        arg = f(std::move(arg));

    return arg; 
}

both functions are perfectly valid c++ code, and both involve calling the copy/move assignment operator, as function parameters can't be copy-elided.

Sign up to request clarification or add additional context in comments.

6 Comments

What if a const variable was passed as arg?
@عليالمطوع the template won't compile, because T will be deduced as const Y and you can't call its copy assignment operator as the operator is not const.
So is taking a copy in this case the best solution?
@عليالمطوع if you don't want to modify the input you should take the input by value, if you are okay with modifying the input then the forwarding references version works okay, as no extra copies will be made, only a lot of moves.
Okay this makes sense. Thank you Ahmed!
|
2

It's invalid. If you passed an rvalue and f happens to return the same reference it was given, you could end up with self-move-assignment, which classes are generally not expected to handle correctly (unlike self-copy-assignment).

I'm not entirely sure how to handle this. Something along the lines of:

if constexpr (std::is_same_v<decltype(f(std::move(arg))), std::remove_reference_t<T> &&>)
    arg = auto(f(std::move(arg)));`.
else
    arg = f(std::move(arg));

Or, if you can't be bothered, simply arg = f(arg).

Forwarding rather than moving arg doesn't make sense, since we don't need to preserve the old value, as we're going to overwrite it immediately anyway.


Also it's weird to pass arg by reference, but then to return by value.

I would suggest either passing by value and returning by value, or doing both by reference.

Comments

2

Function templates are a form of overloading. Overloading is having the same function name refer to more than one function.

The key to make overloading sane is to make every overload have the same semantic meaning. Then the selection of which specific implementation, being not visible to the caller without extreme effort, is less likely to cause them to go insane or cause crazy bugs.

Your applyTenTimes, despite having the same implementation (up to the type parameter) doesn't really work that way. If passed an lvalue, the first parameter is an in-out argument; if passed an rvalue, it is an in only parameter; in both cases, it is used as temporary swap space and is left in a strange state at the end of the operation.

A sane version of your template is:

template <typename T>
requires !std::is_reference_v<T>
[[nodiscard]] T applyTenTimes(T arg, auto&& f){
  for(int i = 0; i < 10; ++i)
    arg = f(std::move(arg));

  return arg; 
}

or even just

[[nodiscard]] auto applyTenTimes(auto arg, auto&& f){
  for(int i = 0; i < 10; ++i)
    arg = f(std::move(arg));

  return arg; 
}

this always takes arg by value, then uses it as a temporary internally, then returns the result. It avoids a number of traps (like the risks of move self-assign, or const-assign-to).

The input argument is not used as swap space. f does not get a reference parameter it thinks is meaningful, which is semantically accurate.

1 Comment

Your comment and code example is exactly what I was looking for. Thank you, Adam!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.