Why did C++11 make std::string::data() add a null terminating character?

There are two points to discuss here:

Space for the null-terminator

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.

The null-terminator itself

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.


Advantages of the change:

  1. 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.

  2. 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.

  3. 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.


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.