Reputation: 21513
Previously that was std::string::c_str()
's job, but as of C++11, data()
also provides it, why was c_str()
's null-terminating-character added to std::string::data()
? To me it seems like a waste of CPU cycles, in cases where the null-terminating-character is not relevant at all and only data()
is used, a C++03 compiler doesn't have to care about the terminator, and don't have to write 0 to the terminator every time the string is resized, but a C++11 compiler, because of the data()
-null-guarantee, has to waste cycles writing 0 every time the string is resized, so since it potentially makes code slower, I guess they had some reason to add that guarantee, what was it?
Upvotes: 29
Views: 5028
Reputation: 385194
Actually, it's the other way around.
Before C++11, c_str()
may in theory have cost "additional cycles" as well as a copy, so as to ensure the presence of a null terminator at the end of the buffer.
This was unfortunate, particularly as it can be fixed very simply, with effectively no additional runtime cost, by simply incorporating a null byte at the end of every buffer to begin with. Only one additional byte to allocate (and a teensie little write), with no runtime cost at point of use, in exchange for thread-safety and a boatload of sanity.
Once you've done that, c_str()
is literally the same as data()
by definition. So, the "change" to data()
actually came for free. Nobody's adding an extra byte to the result of data()
; it's already there.
Helping matters is the fact that most implementations already did this under C++03 anyway, to avoid the hypothetical runtime cost ascribed to c_str()
.
So, in short, this has almost certainly cost you literally nothing.
Upvotes: 2
Reputation: 238361
Advantages of the change:
When data
also guarantees the null terminator, the programmer doesn't need to know obscure details of differences between c_str
and data
and consequently would avoid undefined behaviour from passing strings without guarantee of null termination into functions that require null termination. Such functions are ubiquitous in C interfaces, and C interfaces are used in C++ a lot.
The subscript operator was also changed to allow read access to str[str.size()]
. Not allowing access to str.data() + str.size()
would be inconsistent.
While not initialising the null terminator upon resize etc. may make that operation faster, it forces the initialisation in c_str
which makes that function slower¹. The optimisation case that was removed was not universally the better choice. Given the change mentioned in point 2. that slowness would have affected the subscript operator as well, which would certainly not have been acceptable for performance. As such, the null terminator was going to be there anyway, and therefore there would not be a downside in guaranteeing that it is.
Curious detail: str.at(str.size())
still throws an exception.
P.S. There was another change, that is to guarantee that strings have contiguous storage (which is why data
is provided in the first place). Prior to C++11, implementations could have used roped strings, and reallocate upon call to c_str
. No major implementation had chosen to exploit this freedom (to my knowledge).
P.P.S Old versions of GCC's libstdc++ for example apparently did set the null terminator only in c_str
until version 3.4. See the related commit for details.
¹ A factor to this is concurrency that was introduced to the language standard in C++11. Concurrent non-atomic modification is data-race undefined behaviour, which is why C++ compilers are allowed to optimize aggressively and keep things in registers. So a library implementation written in ordinary C++ would have UB for concurrent calls to .c_str()
In practice (see comments) having multiple threads writing the same thing wouldn't cause a correctness problem because asm for real CPUs doesn't have UB. And C++ UB rules mean that multiple threads actually modifying a std::string
object (other than calling c_str()
) without synchronization is something the compiler + library can assume doesn't happen.
But it would dirty cache and prevent other threads from reading it, so is still a poor choice, especially for strings that potentially have concurrent readers. Also it would stop .c_str()
from basically optimizing away because of the store side-effect.
Upvotes: 25
Reputation: 26076
There are two points to discuss here:
In theory a C++03 implementation could have avoided allocating space for the terminator and/or may have needed to perform copies (e.g. unsharing).
However, all sane implementations allocated room for the null-terminator in order to support c_str()
to begin with, because otherwise it would be virtually unusable if that was not a trivial call.
It is true that some very (1999), very old implementations (2001) wrote the \0
every c_str()
call.
However, major implementations changed (2004) or were already like that (2010) to avoid such a thing way before C++11 was released, so when the new standard came, for many users nothing changed.
Now, whether a C++03 implementation should have done it or not:
To me it seems like a waste of CPU cycles
Not really. If you are calling c_str()
more than once, you are already wasting cycles by writing it several times. Not only that, you are messing with the cache hierarchy, which is important to consider in multithreaded systems. Recall that multi-core/SMT CPUs started to appear between 2001 and 2006, which explains the switch to modern, non-CoW implementations (even if there were multi-CPU systems a couple of decades before that).
The only situation where you would save anything is if you never called c_str()
. However, note that when you are re-sizing the string, you are anyway re-writing everything. An additional byte is going to be hardly measurable.
In other words, by not writing the terminator on re-size, you are exposing yourself to worse performance/latency. By writing it once at the same time you have to perform a copy of the string, the performance behavior is way more predictable and you avoid performance pitfalls if you end up using c_str()
, specially on multithreaded systems.
Upvotes: 28
Reputation: 26496
The premise of the question is problematic.
a string class has to do a lot of expansive things, like allocating dynamic memory, copying bytes from one buffer to another, freeing the underlying memory and so on.
what upsets you is one lousy mov
assembly instruction? believe me, this doesn't effect your performance even by 0.5%.
When writing a programing language runtime, you can't be obsessive about every small assembly instruction. you have to choose your optimization battles wisely, and optimizing an un-noticable null termination is not one of them.
In this specific case, being compatible with C is way more important than null termination.
Upvotes: 13