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:
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:
std::string
(with another http client that returned std::string
as body).std::move
.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
.
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.