1

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.

Quirin F. Schroll
  • 1,302
  • 1
  • 11
  • 25
  • In general with most C++ standard library objects, since you **move** an object, it is left behind in a valid-but-unspecified state (suitable for destruction, or reassignment). (Some classes have additional guarantees, like `std::vector` or `std::unique_ptr`.) I think there are no guarantees which side of the `{ a, b }` arguments are evaluated first (it's an implementation detail). Oh the details are confusing, [list initialization](https://en.cppreference.com/w/cpp/language/list%20initialization). Ow, my brain. – Eljay Apr 21 '23 at 17:36
  • 2
    `pair(_T1 const& __t1, _T2 const& __t2)` ... looks like the constructor is getting the rug pulled out from under it, because it's a const-ref to the parameters. But the `move` in the second argument is gutting the `r` before the constructor sees the `__t1`. – Eljay Apr 21 '23 at 18:22
  • I'm guessing that Microsoft's STL doesn't bother "emptying" a moved-from string if SSO is in effect. – StoryTeller - Unslander Monica Apr 21 '23 at 18:49

2 Answers2

1

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.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
0

Because the order of the evaluation inside a braced initializer is unspecified. GCC and Clang seem to evaluate "s" + std::move(r) first, after that r is empty.

Order of evaluation

21) Every expression in a comma-separated list of expressions in a parenthesized initializer is evaluated as if for a function call (indeterminately-sequenced)

273K
  • 29,503
  • 10
  • 41
  • 64
  • 3
    The OP isn't using a parenthesized initializer. They are using list initialization which has: *In list-initialization, every value computation and side effect of a given initializer clause is sequenced before every value computation and side effect associated with any initializer clause that follows it in the brace-enclosed comma-separated list of initializers.* – NathanOliver Apr 21 '23 at 18:04
  • @NathanOliver If you look at [Direct-list-initialization](https://en.cppreference.com/w/cpp/language/list_initialization), you see that Direct-list-initialization is initialization of a named variable with a *braced-init-list*. – 273K Apr 21 '23 at 18:46
  • Yeah, *braced*, not *parenthesized*. A parenthesized initializer is `T(foo)`, not `T{foo}`. – NathanOliver Apr 21 '23 at 19:00
  • Got it, I misread it. Will look for a proper reference. – 273K Apr 21 '23 at 20:28