Reputation: 2054
Suppose I have a class which has a std::string
member, and I want to take the value for this member in one of its constructors.
One approach is to take a parameter of type std::string
and then use std::move
:
Foo(std::string str) : _str(std::move(str)) {}
As far as I understand it, moving a string just copies its internal pointers meaning its basically free, so passing a const char*
will be as efficient as passing a const std::string&
.
However in C++17 we got std::string_view
, with its promise of cheap copies. So the above could be written as:
Foo(std::string_view str) : _str(str.begin(), str.end()) {}
No need for moves or constructing temporary std::string
's, but I think it actually just does effectively the same thing as before.
So is there something I am missing here? Or is it just a matter of style as to whether you use std::string_view
or std::string
with move?
Upvotes: 5
Views: 4748
Reputation: 474536
No need for moves or constructing temporary std::string's, but I think it actually just does effectively the same thing as before.
That entirely depends on what the user has when they call your constructor. So lets consider your case 1 (std::string
) and case 2 (std::string_view
). In both cases, the eventual result is a std::string
. Also, this analysis will ignore small string optimization.
So here are some options we can look at:
The user has a string literal.
in case 1, there will be a copy into a std::string
parameter, followed by a move into the std::string
in the class.
In case 2, there will be a copy of a pointer and size, followed by a copy of the characters into the std::string
in the class.
In both cases, the length of the literal must be computed via char_traits::length
at some point. If the user uses a UDL ("some_string"s
or "some_string"sv
) to compute the argument before passing it, then you can avoid the runtime char_traits::length
call.
So in this case, they're basically the same.
The user has a std::string
lvalue which they want to keep the value of.
In case 1, there will be a copy into the std::string
parameter, followed by a move into the std::string
member.
In case 2, there will be a copy of a pointer and size into the std::string_view
parameter, followed by a copy of the characters into the std::string
in the class
In both cases, the length is not computed, since std::string
knows its length. Again, in this case, they're the same.
The user has a std::string
value they want to move into the object. So this is either a prvalue or an explicit std::move
.
In case 1, there will be a move-construction of the parameter, followed by the move-construction of the member.
In case 2, there will be a copy of a pointer and size, followed by a copy of the characters into the std::string
member.
See the difference? In case 1, no characters ever get copied; there are only moves. This is because what the user has and what your class needs are identical. So you get the most efficient transfer possible.
In case 2, characters must get copied, because the string_view
parameter has no idea that the user doesn't want to keep the string around. Therefore, neither does the constructor of the string
member being invoked.
When you use an intermediary for a transfer where the source and destination types are the same, then you can introduce an inefficiency. If the user has the type you actually intend to use, then it is better performance-wise for your interface to express that type directly. If you use a view intermediary, then information and intent between the caller and the callee can get lost.
string_view
is a lingua-franca type; it is primarily intended for when you want to use an array of characters without forcing the user to use a specific string type. For a use case where you intend to keep those characters around beyond the function call, a lingua-franca type is is sub-optimal because the only thing you can do to preserve them is copy them into your own string.
Unless it is important to keep std::string
(or whatever string type you use) out of your interface, or if it's impossible for the user to directly pass the type you're storing the characters in (you may be storing an array, for example), you should use it as the parameter type.
But of course, this is all micro-optimization territory. Unless this class is being used a whole bunch, the difference is trivial.
Upvotes: 5
Reputation: 16700
Let's consider some scenarios:
Foo(std::string s) : str_(std::move(s)) {}
string s1;
Foo("abc"); // A - construct from string literal
Foo (s1); // B - construct from existing string
Foo (string("def")); // C - construct from temporary string
Foo
's constructor, which moves from it.s1
and passes it to Foo
's constructor, which moves from the copy. Foo
's constructor, which moves from it.If instead, we have:
Foo(std::string_view sv) : str_(sv.begin() sv.end()) {}
string s1;
Foo("abc"); // A - construct from string literal
Foo (s1); // B - construct from existing string
Foo (string("def")); // C - construct from temporary string
string_view
(calling strlen
) and passes that. The character data is copied into str_
string_view
from s1.data()
and s1.size()
and passes that. The character data is copied into str_
str_
Neither approach is best in all cases. The first approach works great for (A) and (C), and OK for (B)
The second approach works great for (A), and (B), and not so great for (C).
Upvotes: 4