Why doesn't RVO happen with structured bindings when returning a pair from a function using std::make_pair?
Asked Answered
E

3

17

Consider this code, which defines a simple struct Test (with a default constructor and copy constructor) and returns a std::pair <Test, Test> from a function.

#include <iostream>
#include <utility>

using namespace std;

struct Test {
    Test() {}
    Test(const Test &other) {cout << "Test copy constructor called\n";}
};

auto func() {
    return make_pair(Test(), Test());
}

int main()
{
    auto [a, b] = func(); // Expectation: copies for a and b are both elided

    return 0;
}

Surprisingly, the output is

Test copy constructor called
Test copy constructor called

Whereas modifying func to

auto func() {
    return Test();
}

int main()
{
    Test a(func());

    return 0;
}

Results in the copy constructor not being called. My g++ version is 11.2.0, so I thought that copy elision was guaranteed in this case, but I could be wrong. Could someone confirm if I am misunderstanding RVO?

Eustatius answered 2/7, 2022 at 19:28 Comment(0)
C
19

std::make_pair is a function that takes the arguments by reference. Therefore temporaries are created from the two Test() arguments and std::make_pair constructs a std::pair from these, which requires copy-constructing the pair elements from the arguments. (Move-constructing is impossible since your manual definition of the copy constructor inhibits the implicit move constructor.)

This has nothing to do with structured bindings or RVO or anything else besides std::make_pair.

Because std::pair is not an aggregate class, you cannot solve this by simply constructing the std::pair directly from the two arguments either. In order to have a std::pair construct the elements in-place from an argument list you need to use its std::piecewise_construct overload:

auto func() {
    return std::pair<Test, Test>(std::piecewise_construct, std::forward_as_tuple(), std::forward_as_tuple());
}
Cnidoblast answered 2/7, 2022 at 19:38 Comment(0)
S
8

make_pair is not a type; it's a function. And functions take arguments. Those arguments have to be objects or references to objects. So the prvalues you pass as arguments will manifest temporaries, which are then used to initialize the prvalue returned from make_pair.

And since your type is copy-only, it will do so via the copy constructor of your type.

Susiesuslik answered 2/7, 2022 at 19:38 Comment(0)
W
5

This is not about RVO. When you call

return make_pair(Test(), Test());

the return value is a pair, the pair is return value optimized so there is no copy of the pair being made.

The problem is that you call the function make_pair with 2 objects of type Test. To make the pair the 2 arguments must be copied into the resulting pair. So what you are lacking is AVO, argument value optimization, something C++ doesn't have.

You can avoid this by constructing a pair piecewise:

auto func() {
    return pair<Test, Test>{std::piecewise_construct, tuple(), tuple()};
}

In this syntax you pass the arguments for the constructors instead of already constructed objects. That way the pair can construct the two Test objects into the RVO optimized location without copying.

Drawback is that now you have to worry about copying the constructor arguments.

Generally don't call any of the STL emplace_* or make_* helpers with already created objects if you don't want them to copy or move. They are best used when you can call them with the arguments for the constructors so they construct the objects in-place.

Waksman answered 2/7, 2022 at 20:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.