What is the best way to return multiple large objects in C++?

Viewed 262

I want to return a tuple containing types like std::vector or std::unordered_map etc. where the objects may be large enough that I care about not copying. I wasn't sure how copy elision / return value optimization will work when the returned objects are wrapped in a tuple. To this end I wrote some test code below and am confused by parts of its output:

#include <tuple>
#include <iostream>

struct A {
    A() {}

    A(const A& a) {
        std::cout << "copy constructor\n";
    }

    A(A&& a) noexcept {
        std::cout << "move constructor\n";
    }

    ~A() {
        std::cout << "destructor\n";
    }
};

struct B {

};

std::tuple<A, B> foo() {

    A a;
    B b;
    return { a, b };
}

std::tuple<A, B> bar() {
    A a;
    B b;
    return { std::move(a), std::move(b) };
}

std::tuple<A, B> quux() {
    A a;
    B b;
    return std::move(std::tuple<A, B>{ std::move(a), std::move(b) });
}

std::tuple<A, B> mumble() {
    A a;
    B b;
    return std::move(std::tuple<A, B>{ a, b });
}

int main()
{  
    std::cout << "calling foo...\n\n";
    auto [a1, b1] = foo();
    std::cout << "\n";

    std::cout << "calling bar...\n\n";
    auto [a2, b2] = bar();
    std::cout << "\n";

    std::cout << "calling quux...\n\n";
    auto [a3, b3] = quux();
    std::cout << "\n";

    std::cout << "calling mumble...\n\n";
    auto [a4, b4] = mumble();
    std::cout << "\n";

    std::cout << "cleaning up main()\n";

    return 0;
}

when I run the above (on VS2019) I get the following output:

calling foo...
copy constructor
destructor

calling bar...
move constructor
destructor

calling quux...
move constructor
move constructor
destructor
destructor

calling mumble...
copy constructor
move constructor
destructor
destructor

cleaning up main()
destructor
destructor
destructor
destructor

So from the above is looks like bar() is best which is return { std::move(a), std::move(b) }. My main question is why foo() ends up copying? RVO should elide the tuple from being copied but shouldn't the compiler be smart enough to not copy the A struct? The tuple constructor could be a move constructor there since it is firing in an expression that is being returned from a function i.e. because struct a is about to not exist.

I also don't really understand what is going on with quux(). I didnt think that additional std::move() call was necessary but I don't understand why it ends up causing an additional move to actually occur i.e. I'd expect it to have the same output as bar().

1 Answers

My main question is why foo() ends up copying? RVO should elide the tuple from being copied but shouldn't the compiler be smart enough to not copy the A struct? The tuple constructor could be a move constructor

No, move constructor could only construct it from another tuple<> object. {a,b} is constructing from the component types, so the A and B objects are copied.

what it going on with quux(). I didnt think that additional std::move() call was necessary but I don't understand why it ends up causing an additional move to actually occur i.e. I'd expect it to have the same output as bar().

The 2nd move happens when you are moving the tuple. Moving it prevents the copy elision that occurs in bar(). It is well-know that std::move() around the entire return expression is harmful.

Related