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?