There are 2 main reasons: string_view is a slice in an existing buffer, it does not require a memory allocation. string_view is passed by value, not by reference.
string_view can improve performance if used correctly, but provides many possibilities to shoot yourself in the foot. If in doubt, stick with string s.
Using std::string_viewThis does not construct a std::string , but merely a lightweight view over the const char* . So no more performance impact. But we still get all the nice things of std::string 's API in order to write expressive code in the implementation of log .
string_view is useful when you want to avoid unnecessary copies. String_views are less memory-intensive to construct and copy. The creation of string_view from literals does not require a dynamic allocation.
std::string_view
is faster in a few cases.
First, std::string const&
requires the data to be in a std::string
, and not a raw C array, a char const*
returned by a C API, a std::vector<char>
produced by some deserialization engine, etc. The avoided format conversion avoids copying bytes, and (if the string is longer than the SBO¹ for the particular std::string
implementation) avoids a memory allocation.
void foo( std::string_view bob ) {
std::cout << bob << "\n";
}
int main(int argc, char const*const* argv) {
foo( "This is a string long enough to avoid the std::string SBO" );
if (argc > 1)
foo( argv[1] );
}
No allocations are done in the string_view
case, but there would be if foo
took a std::string const&
instead of a string_view
.
The second really big reason is that it permits working with substrings without a copy. Suppose you are parsing a 2 gigabyte json string (!)². If you parse it into std::string
, each such parse node where they store the name or value of a node copies the original data from the 2 gb string to a local node.
Instead, if you parse it to std::string_view
s, the nodes refer to the original data. This can save millions of allocations and halve memory requirements during parsing.
The speedup you can get is simply ridiculous.
This is an extreme case, but other "get a substring and work with it" cases can also generate decent speedups with string_view
.
An important part to the decision is what you lose by using std::string_view
. It isn't much, but it is something.
You lose implicit null termination, and that is about it. So if the same string will be passed to 3 functions all of which require a null terminator, converting to std::string
once may be wise. Thus if your code is known to need a null terminator, and you don't expect strings fed from C-style sourced buffers or the like, maybe take a std::string const&
. Otherwise take a std::string_view
.
If std::string_view
had a flag that stated if it was null terminated (or something fancier) it would remove even that last reason to use a std::string const&
.
There is a case where taking a std::string
with no const&
is optimal over a std::string_view
. If you need to own a copy of the string indefinitely after the call, taking by-value is efficient. You'll either be in the SBO case (and no allocations, just a few character copies to duplicate it), or you'll be able to move the heap-allocated buffer into a local std::string
. Having two overloads std::string&&
and std::string_view
might be faster, but only marginally, and it would cause modest code bloat (which could cost you all of the speed gains).
¹ Small Buffer Optimization
² Actual use case.
One way that string_view improves performance is that it allows removing prefixes and suffixes easily. Under the hood, string_view can just add the prefix size to a pointer to some string buffer, or subtract the suffix size from the byte counter, this is usually fast. std::string on the other hand has to copy its bytes when you do something like substr (this way you get a new string that owns its buffer, but in many cases you just want to get part of original string without copying). Example:
std::string str{"foobar"};
auto bar = str.substr(3);
assert(bar == "bar");
With std::string_view:
std::string str{"foobar"};
std::string_view bar{str.c_str(), str.size()};
bar.remove_prefix(3);
assert(bar == "bar");
I wrote a very simple benchmark to add some real numbers. I used awesome google benchmark library. Benchmarked functions are:
string remove_prefix(const string &str) {
return str.substr(3);
}
string_view remove_prefix(string_view str) {
str.remove_prefix(3);
return str;
}
static void BM_remove_prefix_string(benchmark::State& state) {
std::string example{"asfaghdfgsghasfasg3423rfgasdg"};
while (state.KeepRunning()) {
auto res = remove_prefix(example);
// auto res = remove_prefix(string_view(example)); for string_view
if (res != "aghdfgsghasfasg3423rfgasdg") {
throw std::runtime_error("bad op");
}
}
}
// BM_remove_prefix_string_view is similar, I skipped it to keep the post short
(x86_64 linux, gcc 6.2, "-O3 -DNDEBUG
"):
Benchmark Time CPU Iterations
-------------------------------------------------------------------
BM_remove_prefix_string 90 ns 90 ns 7740626
BM_remove_prefix_string_view 6 ns 6 ns 120468514
There are 2 main reasons:
string_view
is a slice in an existing buffer, it does not require a memory allocationstring_view
is passed by value, not by referenceThe advantages of having a slice are multiple:
char const*
or char[]
without allocating a new bufferBetter and more consistent performance all over.
Passing by value also has advantages over passing by reference, because aliasing.
Specifically, when you have a std::string const&
parameter, there is no guarantee that the reference string will not be modified. As a result, the compiler must re-fetch the content of the string after each call into an opaque method (pointer to data, length, ...).
On the other hand, when passing a string_view
by value, the compiler can statically determine that no other code can modify the length and data pointers now on the stack (or in registers). As a result, it can "cache" them across function calls.
One thing it can do is avoid constructing an std::string
object in the case of an implicit conversion from a null terminated string:
void foo(const std::string& s);
...
foo("hello, world!"); // std::string object created, possible dynamic allocation.
char msg[] = "good morning!";
foo(msg); // std::string object created, possible dynamic allocation.
std::string_view
is basically just a wrapper around a const char*
. And passing const char*
means that there will be one less pointer in the system in comparison with passing const string*
(or const string&
), because string*
implies something like:
string* -> char* -> char[]
| string |
Clearly for the purpose of passing const arguments the first pointer is superfluous.
p.s. One substancial difference between std::string_view
and const char*
, nevertheless, is that the string_views are not required to be null-terminated (they have built-in size), and this allows for random in-place splicing of longer strings.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With