Search code examples
c++initializationc++20static-membersreturn-value-optimization

Does NRVO happen in static member variables initialization?


I have got a class with a large static std::array that needs an expensive computation to be initialized, so i defined a static method to perform it. I don't know however if some copy happens or maybe the computation is performed inside the member array.

class A
{
  static inline array<double, 100000> a = fill_a();
  static array<double, 100000> fill_a() 
  {
    array<double, 100000> b;  
    /* large computation involving b with non constexpr functions */
    return b;
  }
};

How can I test if b is actually copied? Or if everything is done inside a? Thanks for any help or tip.


Solution

  • I don't know what "NRVO" is. From context I guess you are referring to copy elision. Copy elision is allowed in a few cases. In particular, when returning a non-volatile local variable with the same type as the function's return type, copy elision is permitted (see [class.copy.elision] paragraph 1.1.

    With C++17 copy-elision became mandatory in some contexts. However, in all of these cases the elided copy would copy a prvalue which isn't the case for a named variable, obviously. Thus, the compiler can elide the copy but it isn't mandated to do so. It is a Qualify of Implementatation issue.

    As the effect of copy elision is that the object in question is constructed in the destination location and the destination location is know (in this case &S::b) it should be possible to assert() in cases where copy elision is not used, e.g.:

    #include <algorithm>
    #include <cassert>
    #include <array>
    
    struct S
    {   
        static std::array<double, 10> fill_a() {
            std::array<double, 10> b;
            assert(&a == &b);
            std::fill(b.begin(), b.end(), 0); 
            return b;
        }   
        static inline std::array<double, 10> a = fill_a();
    };  
    
    int main()
    {   
        return S::a[0]; // reference S::a
    }
    

    Interestingly, the above code does assert() although I think g++ normally implements copy elision in these cases. It is, unfortunately, quite possible that adding the assert(&a == &b), or more precisely taking the address of the returned object, inhibits copy elision.

    Otherwise, it would be necessary to observe side-effects of copying to determine that copy elision did not happen. Using std::array<double, N> doesn't have any observable side-effects. It is possible to use a custom class, though, e.g.:

    #include <algorithm>
    #include <iostream>
    #include <cassert>
    #include <cstdlib>
    #include <array>
    
    struct C {
        int i = 0;
        C() = default;
        C(C &&){ std::cout << "moved\n"; };
        C(C const&){ std::cout << "copied\n"; };
    };
    struct S
    {
        static std::array<C, 10> fill_a() {
            std::array<C, 10> b;
            c = &b;
            // assert(&a == &b);
            return b;
        }
        static inline std::array<C, 10>  a = fill_a();
        static inline std::array<C, 10>* c = nullptr;
    };
    
    int main()
    {
        assert(S::c == &S::a);
        return S::a[0].i; // reference S::a
    }
    

    That doesn't print anything about copying or moving C objects, even though the address of the local variable is taken and even the assert() in main() holds. However, when uncommenting the assert in fill_a() it fires! It can be verified that the side-effects would do the Right Thing by kind of making copy elision impossible, e.g., using

    std::array<C, 10> fill_a() {
        std::array<C, 10> b, d;
        return std::rand() % 2? b: d;
    }