Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do I not get guaranteed copy elision with std::tuple?

I would expect that in C++20 the following code prints nothing between prints of A and B (since I expect guaranteed RVO to kick in). But output is:

A

Bye

B

C

Bye

Bye

So presumably one temporary is being created.

#include <iostream>
#include <tuple>
struct INeedElision{
    int i;
    ~INeedElision(){
        std::cout << "Bye\n";
    }
};

std::tuple<int, INeedElision> f(){
    int i = 47;
    return {i, {47}};
}

INeedElision g(){
    return {};
}

int main()
{   
    std::cout << "A\n"; 
    auto x = f();
    std::cout << "B\n";
    auto y = g();
    std::cout << "C\n";
}

What is the reason for this behavior? Is there a workaround to avoid copy (without using pointers)?

https://godbolt.org/z/zasoGd

like image 798
NoSenseEtAl Avatar asked Aug 24 '20 11:08

NoSenseEtAl


2 Answers

When constructing std::tuple<int, INeedElision> from {i, {47}}, the selected constructor of std::tuple takes elements by lvalue-reference to const.

tuple( const Types&... args );

Then when use {i, {47}} as the initializer, a temporary INeedElision will be constructed and then passed to the constructor of std::tuple (and get copied). The temporary object will be destroyed immediately and you'll see "Bye" between "A" and "B".

BTW: The 3rd constructor of std::tuple won't be used for this case.

template< class... UTypes >
tuple( UTypes&&... args );

It's a constructor template, and braced-init-list like {47} doesn't have type and can't be deduced by template argument deduction.

On the other hand, if INeedElision has a converting constructor taking int, and make the initializer as {i, 47}, the 3rd constructor of std::tuple will be used and no temporary INeedElision is constructed; the element will be constructed in-place from the int 47.

LIVE

like image 88
songyuanyao Avatar answered Nov 16 '22 08:11

songyuanyao


you only get copy elision if you return the object itself :

std::vector<int> fn1()
{
   return std::vector<int>{}; // guaranteed copy elision
}

std::vector<int> fn2()
{
   std::vector<int> vec;
   return vec; // a good compiler will manage to elide the copy/move here
}

in your case you are returning tuple so the tuple itself maybe copy elided but not the arguments passed to the constructor of the tuple !

std::tuple<int, INeedElision> f(){

    int i = 47;
    return {i, {47}}; // construct the tuple in place of the return address but the arguments are copied into the tuple and not even moved ! to move call std::move explicitly
}

the compiler isn't allowed to elide the copy of arguments passed to the tuple constructor because you aren't returning the arguments themselves but rather the tuple containing copy of them . Also note that the table can't hold references to the arguments because these local variables will have been destructed by the time the function returns resulting in a dangling references .

if you want to get a chance for copy elision in c++ 17 and later do something like this :

std::tuple<int, INeedElision> f(){

    std::tuple<int, INeedElision> ret;
    auto& [i, ne] = ret;
    i = 47;
    ne = 47;
    return ret;
}
like image 1
dev65 Avatar answered Nov 16 '22 09:11

dev65