c++abirvonrvo

How does C++ ABI deal with RVO and 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.

Solution

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