Search code examples
c++optimizationobject-lifetimereturn-value-optimization

Return value optimization for string_view inside shared_ptr


It's hard to put into words so I will directly jump into a semi-pseudo-code.

I have a download function (http GET), that is being called many many times inside my main code.

std::string download_data(){
    std::shared_ptr<HttpResponse> response = some_http_client->send_request("some_link");
    return std::string(response->body()); // response->body() is a std::string_view.
}

The http_client that I am using, returns a shared_ptr as a response, this response (I excluded the code for HTTP error handling, assume its 200.), contains a response->body(), which is a std::string_view.

This code works fine, however, I want to make sure that the downloaded data isn't copied everytime that this function is called / returned.

My main questions:

  • Is the current code I use, subject to return value optimization ? (Is there anything needs to be done ?)
  • If not, can I just return return response->body(); ? Is a string_view inside a shared_ptr valid after function returns ?

Things I considered, or used in older versions of my code:

  • Returning std::string (with another http client that returned std::string as body).
  • Returning with std::move.
  • Instead of writing a function, just replace all the places this function is called by the body of the function, to directly use the response->body, avoiding a return (I hated it).

What is the correct way of doing this ?

My toolchain:

Ubuntu 20.04 (GLIBC 2.31), g++ 10.2, C++20.


Solution

  • Your code will use RVO. It returns a temporary of the same type as the function returns, which is one of the cases where RVO is mandatory.

    Of course, it will still require one copy of the data, as part of the string constructor that accepts a string_view as an argument.

    You cannot just pass the string_view on its own. It is nothing more than a pair of pointers into someone elses data. Based on your code, that is almost certainly data owned by response, which will expire before you can use the string_view that got returned.

    You basically have two options. You can either copy the data, or preserve it. Your current code copies it once (thanks to RVO), so it's as ideal as we can get for that case. However, there is another approach. We can return an "aliased" shared pointer to the string view. Make your function return a std::shared_ptr<std::string_view>, and we'll set things up to make that work.

    The aliasing constructor of shared_ptr<T> looks like this:

    template <typename Y>
    shared_ptr(const shared_ptr<Y>& custodian, T* ward)
    

    It creates a shared_ptr which, when dereferenced, points at the ward. However, it "owns" the custodian, which can be of any other type. The custodian will not be destroyed until after this shared pointer is destroyed.

    To use this, we have to create a new class which wraps a shared_ptr<HttpResponse> and a body string_view which references data in that response. I'll name it BodyCustodian to make the naming consistent as possible.

    struct BodyCustodian
    {
        BodyCustodian(const std::shared_ptr<HttpResponse>& response)
        : response(response)
        , body(response->body()
        { }
    
        std::shared_ptr<HttpResponse> response;
        std::string_view              body;
    };
    

    Now, in your code, you'll want to create one of these BodyCustodian objects which holds onto its own response (so that the characters behind the body never expire) and a body which is the actual string_view you want to return. We construct one of these, then use the aliasing shared_ptr constructor to create a pointer to body (an element of the BodyCustodian that is valid as long as the custodian is alive), which "owns" the custodian.

    std::shared_ptr<std::string_view>, download_data(){
        std::shared_ptr<BodyCustodian> custodian = std::make_shared<BodyCustodian>(some_http_client->send_request("some_link"));
        return std::shared_ptr<std::string_view>(custodian, &custodian->body);
    }
    

    This shared pointer owns the custodian (which keeps the response alive), so that the body string view is still valid.

    This approach does require creating a small object (~6 pointers in size) on the heap. This is typically fast, and does not depend on the length of the body (which was the issue you were worried about when copying into a std::string). I use make_shared here to make sure I create one ~6 pointer sized object, rather than allocating ~4 pointers worth of space for a BodyCustodian and then ~2 pointers worth of space for the shared_ptr control block. make_shared is smart enough to do them together.