3

Consider the following code:

struct Foo
{
    Foo() { cout << "Foo()\n"; }
    ~Foo() { cout << "~Foo()\n"; }
    Foo(Foo&) { cout << "Foo(Foo&)\n"; }
    Foo(Foo&&) { cout << "Foo(Foo&&)\n"; }

    int d;
};

struct Bar
{
    Foo bigData;
    void workOnBigData() { /*...*/ }
}

Foo getBigData()
{
    Bar b;
    b.workOnBigData();
    return b.bigData;
}

What is the best way to implement getBigData() in terms of copy ellision / move semantics? In this implementation the compiler seems not to be allowed to move bigData. I tested the following functions:

Foo f()
{
    Foo foo;
    return foo;  // RVO
}

Foo g()
{
    Bar b;
    return b.bigData;  // Copy
}

Foo h()
{
    Bar b;
    auto r = move(b.bigData);
    return r;  // Move
}

Can you explain the results from these implementations and show the most effective way to return a member of a local object.

hansmaad
  • 18,417
  • 9
  • 53
  • 94
  • 5
    For named return value optimization, the caller has to reserve some stack space prior to calling the function. This memory will then be used to *construct the local object of the callee in*, the return-statement becomes a no-op. Therefore, NRVO cannot be applied to subobjects, only to complete objects (it has additional restrictions). – dyp Jul 20 '15 at 07:05
  • 4
    Automatic *move on return* is an optimization which changes the observable behaviour for a program. It has been introduced conservatively, i.e. only applies in cases where NRVO is allowed to be applied (but the compiler is unable to) and some other corner cases. Had it not been introduced, then people might have often written `return std::move(x);` even when `x` could be returned via NRVO; but `move(x)` prevents NRVO, so this would have been a pessimisation (for move-only types, `return x` must also check for the move ctor if NRVO shall be applied). – dyp Jul 20 '15 at 07:12
  • @dyp does this mean, that whenever NRVO cannot be applied `return std::move(x)` should be used to enforce move on return (which cannot be done automatically in these cases)? – hansmaad Jul 20 '15 at 07:23
  • 5
    Oh, the "corner cases" from C++11 have been relaxed in C++14 due to [CWG 1579](http://wg21.cmeerw.net/cwg/issue1579). -- *"whenever NRVO cannot be applied"* depends on what you mean with "cannot be applied". If it *may not* be applied, then you should `move`. If it *may* be applied, don't `move`. But the C++14 rule is easier I guess: `move` unless your return statement is `return name;` where `name` is a local (non-static) variable or function parameter. – dyp Jul 20 '15 at 07:30

2 Answers2

3

There are lots of ways to avoid extra copies, the one closer to your code would be imho:

Foo getBigData()
{
    Foo ret; // do a cheap initialization
    Bar b;
    b.workOnBigData();

    std::swap(ret, b.bigData); // 'steal' the member here

    return ret; // NRVO can apply
}

The same can achieved by move constructing the return object

Foo getBigData()
{
    Bar b;
    b.workOnBigData();     
    Foo ret(std::move(b.bigData)); // these two lines are equivalent to
    return ret;                    // return std::move(b.bigData); 
}
Nikos Athanasiou
  • 29,616
  • 15
  • 87
  • 153
  • Why not directly "return std::move(b.bigData); ? – Shakti Malik Aug 11 '22 at 08:21
  • @ShaktiMalik Because `std::move(returnValue)` [prevents RVO](https://stackoverflow.com/q/19267408/2567683). At least that was the state when the answer was given, idk if that is the case now. And creating the value in the caller's frame is usually better. – Nikos Athanasiou Aug 27 '22 at 19:38
1

I think answers to this question Why isn't the copy constructor elided here? are really useful in answering your question. Copy elision is not used in your example

Foo getBigData()
{
    Bar b;
    b.workOnBigData();
    return b.bigData;
}

since this requiment is not fullfiled (http://en.cppreference.com/w/cpp/language/copy_elision):

the return statement's expression is the name of a non-volatile object with automatic storage duration ... and which has the same type (ignoring top-level cv-qualification) as the return type of the function, then copy/move is omitted

In your example Bar is a variable with automatic storage duration but your return Foo. If you change your Bar class a compiler will start using copy elision:

#include <iostream>
#include <typeinfo>

using namespace std;

struct Foo
{
  Foo() { cout << "Foo()\n"; }
  ~Foo() { cout << "~Foo()\n"; }
  Foo(const Foo&) { cout << "Foo(Foo&)\n"; }
  Foo(Foo&&) { cout << "Foo(Foo&&)\n"; }

  int d;
};

struct Bar
{
  Foo bigData;
  void workOnBigData() { /*...*/ }
};

struct Bar2
{
  void workOnBigData(Foo&) { /*...*/ }
};

Foo getBigData()
{
    Bar b;
    b.workOnBigData();
    return b.bigData;
}

Foo getBigData2()
{
    Foo f;
    Bar2 b;
    b.workOnBigData(f);
    return f;
}

int main()
{
  {
    Foo f = getBigData();
  }
 cout << "---" << endl;

  {
    Foo f = getBigData2();
  }
}

#include <iostream>
#include <typeinfo>

using namespace std;

struct Foo
{
  Foo() { cout << "Foo()\n"; }
  ~Foo() { cout << "~Foo()\n"; }
  Foo(const Foo&) { cout << "Foo(Foo&)\n"; }
  Foo(Foo&&) { cout << "Foo(Foo&&)\n"; }

  int d;
};

struct Bar
{
  Foo bigData;
  void workOnBigData() { /*...*/ }
};

struct Bar2
{
  void workOnBigData(Foo&) { /*...*/ }
};

Foo getBigData()
{
    Bar b;
    b.workOnBigData();
    return b.bigData;
}

Foo getBigData2()
{
    Foo f;
    Bar2 b;
    b.workOnBigData(f);
    return f;
}

int main()
{
  {
    Foo f = getBigData();
  }
 cout << "---" << endl;

  {
    Foo f = getBigData2();
  }
}

This is what it outputs:

$ ./a.out     
Foo()
Foo(Foo&)
~Foo()
~Foo()
---
Foo()
~Foo()
Community
  • 1
  • 1