20

I am looking into return value optimization in the case of tuple/ties and the behavior I observe is not as I expected. In the example below I would expect move semantics to kick in, which it does, but there is one copy operation which remains. The output from the below in optimized is:

Test duo output, non_reference tuple
Default constructor invoked
Parameter constructor invoked
Copy constructor invoked
Move Assignment operator invoked
100

The invocation of the copy constructor in making the tuple inside the function seems unnecessary. Is there any way to remove this? I am using the MSVC 2012 compiler.

#include <iostream>
#include <tuple>

class A
{
public:
     int value;
     A() : value(-1)
     {
         std::cout << "Default constructor invoked" << std::endl;
     }

     explicit A(const int v) : value(v)
     {
         std::cout << "Parameter constructor invoked" << std::endl;
     }

     A(const A& rhs)
     {
         value = rhs.value;
         std::cout << "Copy constructor invoked" << std::endl;
     }

     A(const A&& rhs)
     {
         value = rhs.value;
         std::cout << "Move constructor invoked" << std::endl;
     }

     A& operator=(const A& rhs)
     {
         value = rhs.value;
         std::cout << "Assignment operator invoked" << std::endl;
         return *this;
     }

     A& operator=(const A&& rhs)
     {
         value = rhs.value;
         std::cout << "Move Assignment operator invoked" << std::endl;
         return *this;
     }
 };

 std::tuple<A, int> return_two_non_reference_tuple()
 {
     A tmp(100);

     return std::make_tuple(tmp, 99);
 }

 int main(int argc, char* argv[])
 {

      std::cout << "Test duo output, non_reference tuple" << std::endl;    
      A t3;
      int v1;
      std::tie(t3, v1) = return_two_non_reference_tuple();
      std::cout << t3.value << std::endl << std::endl;

      system("pause");
      return 0;
}
Marko Popovic
  • 3,999
  • 3
  • 22
  • 37
thorsan
  • 1,034
  • 8
  • 19

4 Answers4

8

The move constructor will not be called automatically because you are calling

std::make_tuple(tmp, 99);

In this case, tmp is an lvalue. You can use std::move to cast it to an rvalue reference:

return std::make_tuple(std::move(tmp), 99);

This will instruct the compiler to use the move constructor.

Marko Popovic
  • 3,999
  • 3
  • 22
  • 37
5

The copy occurs here:

std::make_tuple(tmp, 99);

Although you can see that tmp might be able to be directly constructed in the tuple, the copy from tmp to the tuple will not be elided. What you really want is a way to pass in arguments for std::tuple to use to construct its internal A and int objects. There isn't such a thing for std::tuple, but there is a way to achieve the same effect.

Since you only have two types, you could use std::pair. This has a std::piecewise_construct constructor, which takes two std::tuples containing the arguments to pass to the constructors of the internal objects.

 std::pair<A, int> return_two_non_reference_tuple()
 {
     return {std::piecewise_construct, 
             std::make_tuple(100), std::make_tuple(99)};
 }

The cool thing about this solution is that you can still use std::tie at the call site, because std::tuple has an assignment operator from std::pair.

std::tie(t3, v1) = return_two_non_reference_tuple();

As you can see from the output, your copy is gone. It isn't replaced by a move like in the other answers, it's totally removed:

Test duo output, non_reference tuple

Default constructor invoked

Parameter constructor invoked

Move Assignment operator invoked

100

Live Demo

Community
  • 1
  • 1
TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • 1
    Thanks, its a good alternative for the simple case, but if there is more actions done on tmp, and more outputs, it wouldnt work. And I dont think it would work in MSVC 2012. – thorsan Mar 11 '16 at 09:32
  • Copy elision is happening here. The problem is that `tmp` is not being moved into the `tuple`. The copy constructor is from copying `tmp` into the tuple, not the tuple being copied. – Simple Mar 11 '16 at 09:33
  • @Simple I know that, maybe my answer isn't clear enough. – TartanLlama Mar 11 '16 at 09:35
  • @thorsan If there are more actions to be done on `tmp`, you could execute them on the element of the pair, then let the return of the pair be elided through NRVO. – TartanLlama Mar 11 '16 at 09:38
  • @TartanLlama no. You cannot rely on (N)RVO as this is merely an optional optimization compilers can employ but are not required to. As a matter of fact. MSVC 17 (visual studio 15.3) doesn't support NRVO in debug mode (only RVO) but it does support both in release mode. (I just tested). However, c++17 introduces guaranteed copy elision. This you CAN rely on (if you compiler supports it yet). It prevents the need to construct (materialize) a temporary all together when it is not needed. – Jupiter Aug 15 '17 at 13:26
  • c++17 even allows you to `auto [t3, v1] = return_two_non_reference_tuple()` (Structured Bindings). – Niclas Larsson Sep 19 '18 at 12:28
3

The copy takes place in return_two_non_reference_tuple().

One way to remove it is to move tmp

std::tuple<A, int> return_two_non_reference_tuple()
{
    A tmp(100);
    return std::make_tuple(std::move(tmp), 99);
}

or another way

std::tuple<A, int> return_two_non_reference_tuple()
{
    return std::make_tuple(A(100), 99);
}
Lukáš Bednařík
  • 2,578
  • 2
  • 15
  • 30
1

You are not moving tmp:

 A tmp(100);

 return std::make_tuple(std::move(tmp), 99);
Simple
  • 13,992
  • 2
  • 47
  • 47