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.
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).