c++braced-init-list

Sequencing of constructor arguments with move semantics and braced initialization?


There are many similar questions on SO, but I haven't found one that gets at this. As we all know, std::move doesn't move. That has the curious advantage of removing a potential read-from-moved-from footgun: if I call the function void f(std::vector<int>, std::size_t) with a std::vector<int> v as f(std::move(v), v.size()), it will evaluate the arguments in some order (unspecified, I think?) but std::move(v) doesn't move v, it just casts to std::vector<int>&&, so even if std::move(v) is sequenced before v.size(), v hasn't been moved from so the size is the same as if it were sequenced first. Right?

I was very surprised, then, to see this logic break down in the very specific case of a braced call to a constructor, but not when I use parens for the constructor, and only when the constructor takes the vector by value. That is

#include <vector>
#include <fmt/core.h>

struct ByValue {
    std::vector<int> v;
    std::size_t x;
    ByValue(std::vector<int> v, std::size_t x) : v(std::move(v)), x(x) {}
};

struct ByRValueRef {
    std::vector<int> v;
    std::size_t x;
    ByRValueRef(std::vector<int>&& v, std::size_t x) : v(std::move(v)), x(x) {}
};

template <typename T>
void test(std::string_view name) {
    {
        auto v = std::vector<int>(42);
        // Construct with braces:
        auto s = T{std::move(v), v.size()};
        fmt::print("{}{{std::move(v), v.size()}} => s.x == {}\n", name, s.x);
    }
    {
        auto v = std::vector<int>(42);
        auto s = T(std::move(v), v.size());
        // Construct with parens:
        fmt::print("{}(std::move(v), v.size()) => s.x == {}\n", name, s.x);
    }
}

std::size_t getXValue(std::vector<int>, std::size_t x) { return x; }
std::size_t getXRValueRef(std::vector<int>&&, std::size_t x) { return x; }

int main() {
    test<ByValue>("ByValue");
    test<ByRValueRef>("ByRValueRef");
    {
        auto v = std::vector<int>(42);
        fmt::print("getXValue -> {}\n", getXValue(std::move(v), v.size()));
    }
    {
        auto v = std::vector<int>(42);
        fmt::print("getXRValueRef -> {}\n", getXRValueRef(std::move(v), v.size()));
    }
}

gcc prints

ByValue{std::move(v), v.size()} => s.x == 0
ByValue(std::move(v), v.size()) => s.x == 42
ByRValueRef{std::move(v), v.size()} => s.x == 42
ByRValueRef(std::move(v), v.size()) => s.x == 42
getXValue -> 42
getXRValueRef -> 42

https://godbolt.org/z/nz3qx8nvK

But then clang disagrees:

ByValue{std::move(v), v.size()} => s.x == 0
ByValue(std::move(v), v.size()) => s.x == 0
ByRValueRef{std::move(v), v.size()} => s.x == 42
ByRValueRef(std::move(v), v.size()) => s.x == 42
getXValue -> 0
getXRValueRef -> 42

https://godbolt.org/z/drbYzoovd

For gcc, the only surprising case is ByValue{std::move(v), v.size()} => s.x == 0. What's going on here? I've assumed that ByValue{...} turns into a "function" (cunstructor) call to ByValue(std::vector<int> v, std::size_t x), which I think means that it evaluates all of the arguments (std::move(v) and v.size()) and then calls the function.

For clang, all of the by-value constructors and function calls get v into a moved-from (empty) state before the corresponding call to v.size().

What's going on? Is it that gcc evaluates right-to-left, except for in braces, and clang always left-to-right, and that if a function takes the vector by value, the argument that gets created is a vector, so it's basically saying std::vector<int>(std::move(v)), v.size(), sequenced in that order?


Solution

  • so even if std::move(v) is sequenced before v.size(), v hasn't been moved from so the size is the same as if it were sequenced first. Right?

    No, not right. The initialization of the function parameter also happens before the function is called in the context of the caller. Since your example uses a by-value std::vector parameter, not a reference, the move construction will happen in the caller and may (or may not) happen before the call to v.size(), resulting in multiple possible outcomes, which you are observing in different behavior by GCC and Clang.

    Using braces changes this in that initialization with braces (regardless of whether it resolves to a constructor call or aggregate initialization) is specified to evaluate all initializers and their associated effects strictly left-to-right. So there is no variation in outcome anymore. The move construction will happen before the v.size() call. You can see this in your example by both GCC and Clang agreeing on 0 as size.

    When you switch to a reference parameter your reasoning becomes correct and the compilers agree with you on the value of size().