I would like to know how to properly handle a string input in a function, if I know I will have to make a copy inside of it to place it inside a container.
I was thinking of doing it like this:
void foo(const string& s){
container.push(s);
}
But if I pass a char* string I will make 2 copies, first when calling foo, then when passing it to a container.
My second idea was to use a string_view, as it is often said, that if you can use a view, you should. So my second version of the function looked like this:
void foo2(string_view s){
container.push(string(s));
}
Ok, now I will make only a single copy no matter what type of string I am given. But then I started thinking, why couldn't I just accept a string as an argument to a function, simplifying it like this:
void foo3(string s){
container.push(std::move(s));
}
But now I have to make sure, that my container properly utilizes move semantics so it doesn't end up making another copy! As this container object is a templated type it means using perfect forwarding and so on, which is whole lot of work in itself.
At this point I have no idea which option to choose, so I would like to ask you for some advice. I also feel like I am way overthinking this, as this probably won't have that much of a performance impact in the end, but I would like to do it right. Thanks in advance.
CodePudding user response:
You know that you definitely need a std::string
in the end. Not something string like, exactly a std::string
. In this case it doesn't make sense to take as argument anything else than a std::string
. There are exceptions to this, e.g. if all of your exposed interface you use std::string_view
then it might make sense to do here as well.
Now the choices are const std::string&
(lvalue ref), std::string&&
(rvalue ref) and std::string
(value). In this case where you know you need a new object to pass to the level below the recommended one is: value std::move
.
This way:
- if the user has something else than a
std::string
you get 1 conversion 1 move - if the user has
std::string
you get 1 copy 1 move (or 1 move 1 move if the user doesstd::move
at the call site).
But now I have to make sure, that my container properly utilizes move semantics
No, not "now". This shouldn't be the point where you start to consider move semantics for your container. A container most likely should have proper move semantics just on the merit of it being ... well a container. You don't want to copy 5k elements when you could have moved them instead.
CodePudding user response:
A fourth option would be to use perfect forwarding and pass the arguments to foo
as-is to the container's emplace
/emplace_back
function:
template<class... Args>
requires std::constructible_from<std::string, Args...>
void foo4(Args&&... args){ // a forwarding reference pack
container.emplace(std::forward<Args>(args)...);
}
- If you pass a temporary
std::string
tofoo4
, it'll use thestd::string
move constructor. Nothing else. - If you pass an lvalue reference to
foo4
, it'll use thestd::string
copy constructor. Nothing else. - If you pass other arguments that can be used to construct a
std::string
, like astd::string_view
, it'll be constructed in-place in the container. Not even a copy or move will be needed.
You could do equally well for the first two cases above by manually creating the overloads needed to avoid unneeded instantiations.
void foo4(const std::string& f) { container.push(f); } // one copy
void foo4(std::string&& f) { container.push(std::move(f)); } // one move
It would however not allow for the the third case to be as effective. If you don't care about, or don't want to support, the third case, you could go for the two overloads that mimics what a copy constructor/move constructor pair does. Since you asked about how to "efficiently pass a string" I think that adding an overload is a small price to pay.