c++nrvo

Does returning a default constructed object prevent NRVO?


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.


Solution

  • 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 ;)