C++11: Do move semantics get involved for pass by value?
Asked Answered
M

3

9

I've got an API that looks like this:

void WriteDefaultFileOutput(std::wostream &str, std::wstring target)
{
    //Some code that modifies target before printing it and such...
}

I'm wondering if it'd be sensible to enable move semantics by doing this:

void WriteDefaultFileOutput(std::wostream &str, std::wstring&& target)
{
    //As above
}
void WriteDefaultFileOutput(std::wostream &str, std::wstring const& target)
{
    std::wstring tmp(target);
    WriteDefaultFileOutput(str, std::move(tmp));
}

or is this just boilerplate that the compiler should be able to figure out anyway?

Macy answered 15/3, 2012 at 17:18 Comment(5)
The wstring has a move constructor, so isn't this what is happening already?Faust
Can't you avoid the tmp and the std::move and pass wstring(target) in the function call directly?Parallelepiped
This is maybe a stupid question, but why is passing a plain normal const reference not an option? Writing something (here: a string) to a stream should not modify it, nor require an extra, private copy. So, I really don't see the reasoning behind first making a (possibly deep) temporal copy and then using (destructive) move semantics on the temporary. Except for the sake of using move semantics, of course.Radferd
@Radferd : A comment in the code shown explicitly says //Some code that modifies target before printing it and such....Slang
Ah, silly me... missed that comment, thank you.Radferd
U
21

"Pass by value" can mean either copy or move; if the argument is an lvalue, the copy constructor is invoked. If the argument is an rvalue, the move constructor is invoked. You don't have to do anything special involving rvalue references to get move semantics with pass by value.

I dig deeper into this topic in What are move semantics?

Unison answered 15/3, 2012 at 17:24 Comment(4)
In the case of pass by value, and an rvalue argument, I'd expect the temporary to be constructed exactly where it was needed, so that there wouldn't be anything to move. Or to copy, in older compilers.Glomeration
Maybe; compiler optimization land is not my territory.Unison
It's hardly what I would call optimization (although officially it is). Eliding of a copy constructor can result in a change in program semantics, is only legal because the standard gives the compiler explicit authorization to do it, and because it may change the semantics, must be taken into account when evaluating program correctness.Glomeration
Anyway, the optimization you mention is only possible with prvalues, not xvalues.Unison
D
3

You should prefer pass-by-value if you're going to make a copy with non-movable values. For example, your const& version of WriteDefaultFileOutput explicitly copies the parameter, then moves the copy with the move version. Which means if WriteDefaultFileOutput is called with a movable value (xvalue or prvalue), then it will be moved, and if it's called with an lvalue, it will be copied.

This means that there is no difference between the two forms of this function. Consider this:

WriteDefaultFileOutput(L"SomeString");

In your first case, it will create a temporary wstring. Temporaries are prvalues, so it will be "moved" into the parameter (since wstring has a move constructor). Of course, any compiler worth its salt will elide the move and simply construct the temporary directly into the parameter.

In your second case, more or less the same thing happens. A temporary wstring is created. Temporaries are prvalues, so they can bind to && parameter types. Therefore, it will call the first version of your function with an r-value reference to the temporary. The only possible difference would be from a compiler that won't elide the move. And even then, a move isn't that expensive (depending on your basic_string implementation).

Now consider this:

std::wstring myStr{L"SomeString"};
WriteDefaultFileOutput(myStr);

In the first case, the call to WriteDefaultFileOutput will cause a copy of the myStr value into the function parameter.

In the second case, myStr is an lvalue. It cannot bind to a && parameter. Therefore, the only version it can call is the const& version. That function will manually construct a copy, and then move the copy with the other one.

An identical effect. Your first version has less code, so go with that for obvious reasons.

In general, I would say that there are only two reasons to take a parameter as a &&:

  1. You are writing a move constructor.
  2. You are writing a forwarding function and need to use perfect forwarding.

In all other cases where you want movement to be possible, just take a value. If the user wants to copy, let them copy. I suppose if you want to explicitly forbid copying of the parameter, you could take a &&. But the main issue is one of clarity.

If you take a value parameter, and the user provides a moveable value, then the user-provided value will always be moved. For example, your && version of WriteDefaultFileOutput does not have to actually move the data from its parameter. It certainly can. But it doesn't have to. If it took a value, then it would already have claimed the data.

Therefore, if a function takes a value parameter, and you see a std::move into that value, then you know that the object which was moved is now empty. It is guaranteed to have been moved from.

Damnable answered 15/3, 2012 at 18:31 Comment(0)
S
0

Slightly correct @fredoverflow answer

Pass by value can mean either copy or move from argument into parameter

  • If the argument is lvalue, the copy constructor is invoked.
  • If the argument is rvalue reference, the move constructor is invoked.
  • If the argument is rvalue, no copy or move is invoked. (Copy elision kicks in)

See https://godbolt.org/z/Mn56cb949

int f(A a) {
    std::cout << __func__ << std::endl;
    return a.x + a.y;
}

A a{1, 2};
// move construct A
f(std::move(a));

// no copy or move
f(A{1, 2});
Sestos answered 27/10, 2023 at 15:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.