Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does C++ ABI deal with RVO and NRVO?

Tags:

c++

abi

rvo

nrvo

I am confused with regards how do compiler and linker deal with the fact that requirements on the caller of the function differ depending on if the function uses RVO or NRVO.

This could be my misunderstanding, but my assumption is that generally without RVO or NRVO

std::string s = get_string();

involves move construction of s from result of get_string if get_string does not do N?RVO but if get_string does N?RVO calling code does nothing and s is constructed inplace by the function get_string.

EDIT: here is how I imagine get_string caller operating if there is no N?RVO:

  1. call get_string()
  2. get_string result is now on the stack, caller uses that to construct s

and now with RVO

  1. call get_string()
  2. when get_string is done there is no result on the stack, get_string constructed s, caller does not need to do anything to construct s.
like image 444
NoSenseEtAl Avatar asked Feb 23 '18 19:02

NoSenseEtAl


People also ask

Does C have RVO?

> Note also that C doesn't have return-value-optimization, hence all your struct-returning functions will cause a call to memcpy (won't happen when compiled in C++ mode of course). What ? RVO is precisely needed because a copy in C++ can run arbitrary code and so is not as easy to ellide as a memcpy.

Is NRVO guaranteed?

Compilers often perform Named Return Value Optimization (NRVO) in such cases, but it is not guaranteed.

What is NRVO?

the NRVO (Named Return Value Optimization)


1 Answers

The caller allocates space for the return object no matter what. From the caller's perspective, it doesn't matter if the function uses RVO or not.

You're also confusing two separate copy elisions. There's RVO, which elides the copy from a function local variable to the return value, and there's another copy from the function return value to the object being initialized that is also often elided.

Basically, without any elision, you can think of the call from the OP as looking something like this (ignore any aliasing issues, this would all actually be implemented directly in assembly):

void get_string(void* retval)
{
    std::string ret;
    // do stuff to ret
    new(retval) std::string(std::move(ret));
}

char retval[sizeof(std::string)];
get_string(retval);
std::string s(std::move(*(string*)retval));

The string ret is copied (or moved, in this case) twice: once from ret to the retval buffer, and once from retval to s.

Now, with NRVO applied, only the definition of get_string would change:

void get_string(void* retval)
{
    std::string& ret = *new(retval) std::string;
    // do stuff to ret
}

From the caller's perspecitve, nothing has changed. The function just directly initializes the object it's going to return into the space allocated by the caller for the return value. Now the string is only moved once: from retval to s.

Now the caller can also elide a copy, since there's no need to allocate a separate return value and then copy it into the object being initialized:

char retval[sizeof(std::string)];
get_string(retval);
std::string& s(*(string*)retval);

In this way, s is initialized directly by get_string, and no copies or moves are performed.

like image 74
Miles Budnek Avatar answered Oct 04 '22 00:10

Miles Budnek