Why does C++ not know to do an implicit move in the return when the variable is used in an initializer list?
Asked Answered
E

1

29

Consider this code:

#include <iostream>
template<typename A>
struct S{
    S(const A& a){
        std::cout << "L\n";
    }
    S(A&& a){
        std::cout << "R\n";
    }
};
S<int> f1(){
    int a = 1;
    return {a};
}
S<int> f2(){
    int a = 1;
    return a;
}
S<int> f3(){
    int a = 1;
    return {std::move(a)};
}
int main()
{
    f1();
    f2();
    f3();
}

Output is

L
R
R

As you may know C++ implicitly moves in the return (in f2). When we do it manually in the initializer list it works (f3), but it is not done automagically by C++ in f1.

Is there a good reason why this does not work, or is it just a corner case deemed not important enough to be specified by the standard?

P.S. I know compilers can (sometimes must) do RVO, but I do not see how this could explain the output.

Expansile answered 19/3, 2021 at 16:37 Comment(1)
I believe in f1 is interpreted as you explicitly construct S<int> from a then return it. While in f2 it is more of return a so compiler interprets it as a move - which it would do were it to return an int (same type as a).Wye
T
28

The nice thing about:

return name;

Is that it's a simple case to reason about: you're obviously returning just an object, by name, there's no other shenanigans going on here at all. And yet, this specific case, has led to patch after patch after patch after patch. So, maybe not so simple after all.

Once we throw in any further complexity on top of that, it gets way more complicated.

With returning an initializer list, we would have to start considering all sorts of other cases:

// obviously can't move
return {name, name};       

// 'name' might refer to an automatic storage variable, but
// what if 'other_name' is an alias to it? What if 'other_name'
// is a separate automatic storage variable but is somehow
// dependent on 'name' in a way that matters?
return {name, other_name}; 

You just... can't know. The only case that we could definitely consider is an initializer list consisting of a single name:

return {name};

That case is probably fine to implicitly move from. But the thing is, in that case, you can just move:

return {std::move(name)};

The problem specifically with the return name; case is that return std::move(name); was sometimes mandatory and sometimes a pessimization and we would like to get the point where you always just write the one thing and get the optimal behavior. There's no such concern here, return {std::move(name)}; can't inhibit copy elision in the same way. So it's just less of an issue to have to write that.

Tiliaceous answered 19/3, 2021 at 16:54 Comment(4)
I guess Bjarne was not lying when he said that 90% of the standardization time is spent thinking about how new features interact with existing features :(Expansile
Also I think the reason for not special casing 1 elem initializer list may not be just that it is not that valuable, it might be that it makes c++ harder to learn, e.g. explaining to people why it works for IL with 1 element, but not with IL with >=2 elemsExpansile
return {name, name}; now my head hurts. I really feel for WG peopleStrict
Author of P1155 and P2266 says: I agree with this answer. :)Abe

© 2022 - 2024 — McMap. All rights reserved.