2

Supposed I have a function like:

using std::vector;

vector<int> build_vector(int n)
{
   if (some_condition(n)) return {};

   vector<int> out;

   for(int x : something())
   {
      out.push_back(x);
   }

   return out;
}

Does the return {} at the beginning of the function prevent NRVO? I'm curious, as it seems like this would be equivalent to the following:

using std::vector;

vector<int> nrvo_friendly_build_vector(int n)
{
   vector<int> out;

   if (some_condition(n)) return out;

   for(int x : something())
   {
      out.push_back(x);
   }

   return out;
}

But it wasn't clear to me whether the compiler is allowed to do NRVO in the first case.

Gordon Bailey
  • 3,881
  • 20
  • 28
  • Are you looking for a language-lawyer answer, citing chapter & verse from the standard? – Eljay Dec 17 '18 at 21:37
  • The as-if rule will give the compiler a lot of leeway. For instance, it could turn the first version into the second one as it doesn't change the observable behavior. That said you'd probably have to check the assembly. – NathanOliver Dec 17 '18 at 21:37
  • but couldn't turning the first into the second change the observable behaviour if a copy ctor with a side effect was elided that otherwise wouldn't be? – Gordon Bailey Dec 17 '18 at 21:42
  • @eljay not necessarily, I'm more curious if this is something I should care about in practice – Gordon Bailey Dec 17 '18 at 21:42
  • If the vector's constructor has a side effect that could potentially be observable by `some_condition`, then the two versions of the code are not identical. I can't think of any observable side effect that instantiating a vector in auto scope can do. In dynamic scope, perhaps with your own custom allocator. But not in auto scope. – Sam Varshavchik Dec 17 '18 at 21:45
  • @GordonBailey If `some_condition` relied on it then it probably wound't be allowed. If it didn't then it's allowed to optimize it. Since RVO/NRVO are allowed the compiler is free to discard a copy/move even if it has side effects. – NathanOliver Dec 17 '18 at 21:50
  • 2
    @GordonBailey I was actually wondering the same thing _(especially since copy elision is not "required" by the standard)_, so I tested it quickly in an online compiler by replacing `std::vector` by a `Widget` struct: V1 (`build_vector`) http://coliru.stacked-crooked.com/a/5e55efe46bfe32f5 and V2 (`nrvo_friendly_build_vector`) http://coliru.stacked-crooked.com/a/51b036c66e993d62. As you can see, in this particular case (no side effects from constructing the `struct` are visible by `some_condition`), V1 calls the move constructor if `some_condition` is false (at least in clang and g++). HTH – maddouri Dec 17 '18 at 22:16
  • @865719 oh interesting! It looks like even with `-O3` there is an extra move ctor and dtor call! Can you make this an answer so that I can accept it? http://coliru.stacked-crooked.com/a/37e9cd5558c16536 http://coliru.stacked-crooked.com/a/b79474463cc8ae44 – Gordon Bailey Dec 18 '18 at 14:47

2 Answers2

2

From https://en.cppreference.com/w/cpp/language/copy_elision

Under the following circumstances, the compilers are permitted, but not required to omit the copy and move (since C++11) construction of class objects even if the copy/move (since C++11) constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. This is an optimization: even when it takes place and the copy/move (since C++11) constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed:

  • In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization".

  • ...

There are no restrictions with early return, so both versions are candidate to NRVO.

Community
  • 1
  • 1
Jarod42
  • 203,559
  • 14
  • 181
  • 302
1

As requested by the OP, here is an adapted version of my comment

I was actually wondering the same thing (especially since copy elision is not "required" by the standard), so I tested it quickly in an online compiler by replacing std::vector by a Widget struct:

struct Widget
{
    int val = 0;
    Widget()              { printf("default ctor\n"); }
    Widget(const Widget&) { printf("copy ctor\n"); }
    Widget(Widget&&)      { printf("move ctor\n"); }

    Widget& operator=(const Widget&) { printf("copy assign\n"); return *this; }
    Widget& operator=(Widget&&)      { printf("move assign\n"); return *this; }

    ~Widget() { printf("dtor\n"); }

    void method(int)
    {
        printf("method\n");
    }
};

V1 using build_vector(): http://coliru.stacked-crooked.com/a/5e55efe46bfe32f5

#include <cstdio>
#include <array>
#include <cstdlib>

using std::array;

struct Widget
{
    int val = 0;
    Widget()              { printf("default ctor\n"); }
    Widget(const Widget&) { printf("copy ctor\n"); }
    Widget(Widget&&)      { printf("move ctor\n"); }

    Widget& operator=(const Widget&) { printf("copy assign\n"); return *this; }
    Widget& operator=(Widget&&)      { printf("move assign\n"); return *this; }

    ~Widget() { printf("dtor\n"); }

    void method(int)
    {
        printf("method\n");
    }
};

bool some_condition(int x)
{
    return (x % 2) == 0;
}

array<int, 3> something()
{
    return {{1,2,3}};
}

Widget build_vector(int n)
{
   if (some_condition(n)) return {};

   Widget out;

   for(int x : something())
   {
      out.method(x);
   }

   return out;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        return -1;
    }
    const int x = atoi(argv[1]);

    printf("call build_vector\n");
    Widget w = build_vector(x);
    printf("end of call\n");
    return w.val;
}

Output of V1

call build_vector
default ctor
method
method
method
move ctor
dtor
end of call
dtor

V2 using nrvo_friendly_build_vector(): http://coliru.stacked-crooked.com/a/51b036c66e993d62

#include <cstdio>
#include <array>
#include <cstdlib>

using std::array;

struct Widget
{
    int val = 0;
    Widget()              { printf("default ctor\n"); }
    Widget(const Widget&) { printf("copy ctor\n"); }
    Widget(Widget&&)      { printf("move ctor\n"); }

    Widget& operator=(const Widget&) { printf("copy assign\n"); return *this; }
    Widget& operator=(Widget&&)      { printf("move assign\n"); return *this; }

    ~Widget() { printf("dtor\n"); }

    void method(int)
    {
        printf("method\n");
    }
};

bool some_condition(int x)
{
    return (x % 2) == 0;
}

array<int, 3> something()
{
    return {{1,2,3}};
}

Widget nrvo_friendly_build_vector(int n)
{
   Widget out;

   if (some_condition(n)) return out;

   for(int x : something())
   {
      out.method(x);
   }

   return out;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        return -1;
    }
    const int x = atoi(argv[1]);

    printf("call nrvo_friendly_build_vector\n");
    Widget w = nrvo_friendly_build_vector(x);
    printf("end of call\n");
    return w.val;
}

Output of V2

call nrvo_friendly_build_vector
default ctor
method
method
method
end of call
dtor

As you can see, in this particular case (no side effects from constructing the struct are visible by some_condition), V1 calls the move constructor if some_condition() is false (at least in clang and gcc, using -std=c++11 and -O2, in Coliru)

Furthermore, as you have noticed, the same behavior seems to happen at -O3 as well.

HTH

ps: When learning about copy elision, you might find Abseil's ToW #11 interesting ;)

maddouri
  • 3,737
  • 5
  • 29
  • 51
  • It should probably be mentioned that C++17 introduces "guaranteed copy elision". However, my (basic) understanding after glancing (very) quickly over https://jonasdevlieghere.com/guaranteed-copy-elision/#guaranteedcopyelision and https://stackoverflow.com/a/38043447/865719 is that it is now possible to have copy elision for non-movable types _(thus allowing return-by-value for those types)_, which, if I'm not mistaken, is irrelevant to the OP's usecase. – maddouri Dec 18 '18 at 19:43