Search code examples
c++c++17move-semantics

Rules for move when constructing a tuple by return


I have the following c++ example (godbolt) that constructs a tuple of MyStruct in the functions foo and bar:

#include <iostream>
#include <tuple>

struct MyStruct {
    MyStruct() = default;
    MyStruct(const MyStruct&) {
        std::cout << "Copy constructor called" << std::endl;
        
    }
    MyStruct(MyStruct&&) noexcept  {
        std::cout << "Move constructor called" << std::endl;
    }
    ~MyStruct() = default;

    MyStruct& operator=(const MyStruct&) = default;
    MyStruct& operator=(MyStruct&&) noexcept = default;
};

std::tuple<MyStruct, MyStruct> foo() {
    return {MyStruct{}, MyStruct{}};
}

std::tuple<MyStruct, MyStruct> bar() {
    return {{}, {}};
}

int main() {
    std::cout << "Foo" << std::endl;
    auto [a, b] = foo();

    std::cout << "Bar" << std::endl;
    auto [c, d] = bar();
    return 0;
}

And produces the following output:

Foo
Move constructor called
Move constructor called
Bar
Copy constructor called
Copy constructor called

When I put this code in c++ insights, it creates the same function for both foo and bar. So, my understanding is that both foo and bar should move the object instead of bar copying it. Does anyone know why the behaviour is different?

This question is similar but it's not the same as I'm wondering why bar copies the value instead of moving it.


Solution

  • cppinsights.io is wrong here, and doesn't produce code that has the same semantics as your original. The reason why return {{}, {}} calls the copy constructor is a combination of std::tuple's weird constructors, and overload resolution. There are two important constructors here:

    tuple( const Types&... args ); // (2)
    
    template< class... UTypes >
    tuple( UTypes&&... args ); // (3)
    

    In both of your return-statements, you are returning prvalues, making (3) a better match because the conversion sequence is shorter (no const added). If possible, this perfect forwarding overload is going to be chosen, and the move constructor is called.

    However, this is not possible for {{}, {}}, because the types in the Utypes pack cannot be inferred from {}. In general, you can only use these brace expressions in a context where the type can be inferred. For example:

    void take_int(int);
    void take_any(auto);
    
    int main() {
        take_int({}); // OK, value initialization of an int
        take_any({}); // ill-formed, cannot infer type from {}
    }
    

    As a consequence, {{}, {}} will use the first constructor, which involves copy construction. We can reproduce this issue like this:

    template <typename ...Ts>
    struct tuple {
        tuple(const Ts&...);
    
        template <typename ...Us>
        tuple(Us&&...);
    };
    
    struct MyStruct {
        MyStruct() = default;
        MyStruct(const MyStruct&);
        MyStruct(MyStruct&&) noexcept;
    };
    
    tuple<MyStruct, MyStruct> foo() {
        return {MyStruct{}, MyStruct{}};
    }
    
    tuple<MyStruct, MyStruct> bar() {
        return {{}, {}};
    }
    

    This code compiles to:

    foo():
        # ...
        call tuple<MyStruct, MyStruct>::tuple<MyStruct, MyStruct>(MyStruct&&, MyStruct&&)@PLT
        # ...
        ret
    bar():
        # ...
        call tuple<MyStruct, MyStruct>::tuple(MyStruct const&, MyStruct const&)@PLT
        # ...
        ret
    

    See live example on Compiler Explorer