Which is better: returning tuple or passing arguments to function as references?

Look at disassemble (compiled with GCC -O3):

It takes more instruction to implement tuple call.

0000000000000000 <returnValues(int, int)>:
   0:   83 c2 64                add    $0x64,%edx
   3:   83 c6 64                add    $0x64,%esi
   6:   48 89 f8                mov    %rdi,%rax
   9:   89 17                   mov    %edx,(%rdi)
   b:   89 77 04                mov    %esi,0x4(%rdi)
   e:   c3                      retq   
   f:   90                      nop

0000000000000010 <returnValuesVoid(int&, int&)>:
  10:   83 07 64                addl   $0x64,(%rdi)
  13:   83 06 64                addl   $0x64,(%rsi)
  16:   c3                      retq   

But less instructions for the tuple caller:

0000000000000000 <callTuple()>:
   0:   48 83 ec 18             sub    $0x18,%rsp
   4:   ba 14 00 00 00          mov    $0x14,%edx
   9:   be 0a 00 00 00          mov    $0xa,%esi
   e:   48 8d 7c 24 08          lea    0x8(%rsp),%rdi
  13:   e8 00 00 00 00          callq  18 <callTuple()+0x18> // call returnValues
  18:   8b 74 24 0c             mov    0xc(%rsp),%esi
  1c:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi
  23:   e8 00 00 00 00          callq  28 <callTuple()+0x28> // std::cout::operator<<
  28:   8b 74 24 08             mov    0x8(%rsp),%esi
  2c:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi
  33:   e8 00 00 00 00          callq  38 <callTuple()+0x38> // std::cout::operator<<
  38:   48 83 c4 18             add    $0x18,%rsp
  3c:   c3                      retq   
  3d:   0f 1f 00                nopl   (%rax)

0000000000000040 <callRef()>:
  40:   48 83 ec 18             sub    $0x18,%rsp
  44:   48 8d 74 24 0c          lea    0xc(%rsp),%rsi
  49:   48 8d 7c 24 08          lea    0x8(%rsp),%rdi
  4e:   c7 44 24 08 0a 00 00    movl   $0xa,0x8(%rsp)
  55:   00 
  56:   c7 44 24 0c 14 00 00    movl   $0x14,0xc(%rsp)
  5d:   00 
  5e:   e8 00 00 00 00          callq  63 <callRef()+0x23> // call returnValuesVoid
  63:   8b 74 24 08             mov    0x8(%rsp),%esi
  67:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi
  6e:   e8 00 00 00 00          callq  73 <callRef()+0x33> // std::cout::operator<<
  73:   8b 74 24 0c             mov    0xc(%rsp),%esi
  77:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi
  7e:   e8 00 00 00 00          callq  83 <callRef()+0x43> // std::cout::operator<<
  83:   48 83 c4 18             add    $0x18,%rsp
  87:   c3                      retq   

I don't think there is any considerable performance different, but the tuple one is more clear, more readable.

Also tried inlined call, there is absolutely no different at all. Both of them generate exactly the same assemble code.

0000000000000000 <callTuple()>:
   0:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi
   7:   48 83 ec 08             sub    $0x8,%rsp
   b:   be 6e 00 00 00          mov    $0x6e,%esi
  10:   e8 00 00 00 00          callq  15 <callTuple()+0x15>
  15:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi
  1c:   be 78 00 00 00          mov    $0x78,%esi
  21:   48 83 c4 08             add    $0x8,%rsp
  25:   e9 00 00 00 00          jmpq   2a <callTuple()+0x2a> // TCO, optimized way to call a function and also return
  2a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

0000000000000030 <callRef()>:
  30:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi
  37:   48 83 ec 08             sub    $0x8,%rsp
  3b:   be 6e 00 00 00          mov    $0x6e,%esi
  40:   e8 00 00 00 00          callq  45 <callRef()+0x15>
  45:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi
  4c:   be 78 00 00 00          mov    $0x78,%esi
  51:   48 83 c4 08             add    $0x8,%rsp
  55:   e9 00 00 00 00          jmpq   5a <callRef()+0x2a> // TCO, optimized way to call a function and also return

Focus on what's more readable and which approach provides a better intuition to the reader, and please keep the performance issues you might think that arise in the background.

A function that returns a tuple (or a pair, a struct, etc.) is yelling to the author that the function returns something, that almost always has some meaning that the user can take into account.

A function that gives back the results in variables passed by reference, may slip the eye's attention of a tired reader.

So, in general, prefer to return the results by a tuple.


Mike van Dyke pointed to this link:

F.21: To return multiple "out" values, prefer returning a tuple or struct

Reason

A return value is self-documenting as an "output-only" value. Note that C++ does have multiple return values, by convention of using a tuple (including pair), possibly with the extra convenience of tie at the call site.

[...]

Exception

Sometimes, we need to pass an object to a function to manipulate its state. In such cases, passing the object by reference T& is usually the right technique.

Tags:

C++

Function