c++returnmovebraced-init-list

Why does an explicit move after use in braced initializer list break the return value?


I had a problem that appeared in GCC and Clang, but not MSVC. The problematic part boils down to this:

#include <utility>
#include <string>
#include <iostream>

auto mkStrings() -> std::pair<std::string, std::string>
{
    std::string r = "r";
    return { r, "s" + std::move(r) }; // !
}

int main()
{
    auto [r, s] = mkStrings();
    std::cout << "'" << r << " " << s << "'" << std::endl;
}

On MSVC, I get:

'r sr'

On GCC 12.2.0 and Clang 15.0.7, it outputs:

' sr'

(On Clang 16.0.1 the compiler segfaults.)

I’m quite convinced the std::move is a problem, but I don’t understand why. I even looked up if I was mistaken that initializer lists are evaluated left-to-right, but according to the answers to this question they are.

Of course, I just removed the std::move, but I’d like to understand why I had to.


Solution

  • The following constructor is used for the initialization:

    template <class U1, class U2>
    constexpr pair(U1&& x, U2&& y);
    

    (In C++23, default template arguments will be added, but this doesn't affect the answer.)

    Because you're using braced initialization, the parameter x will be initialized before the parameter y. However both x and y must be initialized before the ctor-initializer can be evaluated (which actually initializes the first and second members).

    So x is first bound to r, then y to the temporary result of the expression "s" + std::move(r). At this point, r may be moved from.

    Afterward, the constructor uses std::forward<U1>(x) to initialize first. But at this point, the move from the object that x refers to has already been performed.