Return std::tuple and move semantics / copy elision
Asked Answered
K

1

20

I have the following factory function:

auto factory() -> std::tuple<bool, std::vector<int>>
{
    std::vector<int> vec;
    vec.push_back(1);
    vec.push_back(2);

    return { true, vec };
}

auto [b, vec] = factory();

In the return statement is vec considered an xvalue or prvalue and therefore moved or copy elided?

My guess is no, because the compiler, when list-initializing the std::tuple in the return statement, still doesn't know that vec is going to be destroyed. So maybe an explicit std::move is required:

auto factory() -> std::tuple<bool, std::vector<int>>
{
    ...
    return { true, std::move(vec) };
}

auto [b, vec] = factory();

Is it that really required?

Kinnon answered 25/7, 2018 at 14:9 Comment(6)
You have to do return { true, std::move(vec) };.Acicula
Yup, this is required here to be sure that the vector will be moved. But compilers are mostly smart enough to optimize the copy awayMalisamalison
@bartop: Compilers are not allowed to "optimize the copy away".Disable
@NicolBolas Actually, they are allowed to do anything, as long as the observable behaviour does not change. Would be creating a copy observable (provided there are no observable side effects in constructor or destructor)?Entomb
@Aconcagua: Yes, since the allocator::construct/destruct and the copy constructor of the elements of vector are all observable side effects.Disable
@NicolBolas I see, have overseen OS, which can observe: More memory requested, data in memory changes. That's the point, isn't it? Or is there even more involved?Entomb
F
29

In the return statement is vec considered an xvalue or prvalue and therefore moved or copy elided?

vec is always an lvalue. Even in the simple case:

std::vector<int> factory() {
    std::vector<int> vec;
    return vec;
}

That is still returning an lvalue. It's just that we have special rules that say that we just ignore the copy in this case when we're returning the name of an automatic object (and another special rule in the case that copy elision doesn't apply, but we still try to move from lvalues).

But those special rules only apply to the return object; case, they don't apply to the return {1, object}; case, no matter how similar it might look. In your code here, that would do a copy, because that's what you asked for. If you want to do a move, you must do:

return {1, std::move(object)};

And in order to avoid the move, you must do:

auto factory() -> std::tuple<bool, std::vector<int>>
{
    std::tuple<bool, std::vector<int>> t;

    auto& [b, vec] = t;
    b = true;
    vec.push_back(1);
    vec.push_back(2);
    return t;
}
Forrestforrester answered 25/7, 2018 at 14:19 Comment(4)
Perhaps worth noting that in the "avoid moving" variant, the trailing return type can be omitted entirely...Entomb
One-liner variant: return std::make_tuple(true, std::vector<int>({1, 2})); not sure, now, if any moving involved on constructing the return type, though...Entomb
The more interesting point of my one-liner, though, is if I reintroduced moving - which we originally wanted to avoid, i. e. chances are that we cannot use this construct for given purpose...Entomb
Thanks a lot, a very sharp explanation. I tested all the four versions both with GCC and MSVC, the behavior was the expected one.The one-liner of @Entomb is the same as the version with std::move (a part the different constructor).Kinnon

© 2022 - 2024 — McMap. All rights reserved.