c++rvonrvo

Will (N)RVO be applied with my function in this situation?


I have the following code: (ok, in reality it's much more complicated, but I simplified it to make it easier to understand. so please disregard the things that seems stupid. I can't change them in my real situation)

#include <string>

using std::string;

ReportManager g_report_generator;

struct ReportManager
{
    // I know, using c_str in this case is stupid. 
    // but just assume that it has to be this way
    string GenerateReport() { string report("test"); return report.c_str(); }
}

string DoIt(bool remove_all)
{
    if(g_report_generator.isEmpty())
        return string();

    string val = g_report_generator.GenerateReport();

    if(remove_all)
        g_report_generator.clear();

    return val;
}

void main()
{
    string s = DoIt(true);
}

Will (N)RVO be applied with my functions? I did a bit of research, and it would seem like it, but I'm not really convinced and I'd like a second opinion (or more).

I'm using Visual Studio 2017.


Solution

  • To solve your problem I rewrote it.

    #include <string>
    
    
    struct string : std::string {
        using std::string::string;
    
        string(string&& s) {
            exit(-1);
        }
        string(string const&) {
            exit(-2);
        }
    
        string() {}
    };
    
    struct ReportManager
    {
        // I know, using c_str in this case is stupid. 
        // but just assume that it has to be this way
        string GenerateReport()
        {
            string report("test");
            return report.c_str();
        }
        bool isEmpty() const { return true; }
        void clear() const {}
    };
    
    ReportManager g_report_generator;
    
    string DoIt(bool remove_all)
    {
        if(g_report_generator.isEmpty())
            return string();
    
        string val = g_report_generator.GenerateReport();
    
        if(remove_all)
            g_report_generator.clear();
    
        return val;
    }
    
    int main()
    {
        string s = DoIt(true);
    }
    

    The trick with this rewriting is that elision permits skipping copy/move ctors. So every time we actually copy an object (even if inlined), we'll insert an exit clause; only by elision can we avoid it.

    GenerateReport has no (N)RVO or any kind of elision, other than possibly under as-if. I doubt a compiler will be able to prove that, especially if the string is non-static and large enough to require heap storage.

    For DoIt both NRVO and RVO is possible. Elision is legal there, even with side effects.

    MSVC fails -- notice calls to ??0string@@QAE@$QAU0@@Z, which is the move constructor of my local string class.

    When I force the possible RVO case to run by saying it is empty, you'll see that the compiler also fails to RVO optimize here; there is an exit(-1) inlined into the disassembly.

    Clang manages to RVO the return string(); but not NRVO the return val;.

    By far the easiest fix is:

    string DoIt(bool remove_all)
    {
        if(g_report_generator.isEmpty())
            return string();
    
        return [&]{   
          string val = g_report_generator.GenerateReport();
    
          if(remove_all)
            g_report_generator.clear();
    
          return val;
        }();
    }
    

    which has double RVO, and a lambda that does simple NRVO. Zero structural changes to your code, and functions which C++98 compilers can elide return values on (well, they don't support lambda, but you get the idea).