A short visit to std::string

published at 09.11.2026 17:18 by Jens Weller
Save to Instapaper Pocket

Part five in the new C++ content series, visiting std::string. Meeting C++ 2026 is only 2 weeks away!

In the last entry the topic was C++ standard containers. Ending on views and string_view, which brings us to std::string it self. A class that only allocates heap memory when the held block of memory exceeds a certain size. It does so by reusing its own memory, as an std::string is only 32 bytes but operates on two different modes: long string and short string. While long string operates on size_t capacity and size_t size + a pointer to the allocation, short string mode uses some of these bytes to store the short string in its own memory instead of using a heap allocation. A simplified implementation could look like this:

struct long_str
{
size_t size=0,capacity=0;
char* data;
};
struct str
{
std::variant<long_str,std::array<char,24>> var;
};

As strings in C++ are zero terminated, this would be able to reuse 23 bytes of the array to avoid a heap allocation. Though while long_str is 24 bytes in size, str is 32 bytes, the same size as std::string. But using std::variant brings also in a bit of an overhead, and as its been introduced with C++17, std::string wasn’t able to use variant in its design. If we replace std::variant with a union, we get to see its overhead:

struct str
{
union long_short{long_str long_string;std::array<char,24> short_string;};
long_short var;
};

This implementation is now 8 bytes shorter. If one wanted to match the size of std::string, the array for the short string mode could fit 8 more bytes! To keep this simplified example simple, lets assume that allocation is done via new[]/delete[] in long_str.

But the implementation of std::string needs to go one step further, similar to containers it does use allocators for its allocations. So the actual string consists of a compressed pair of a union between long and short string representation and the allocator. A compressed pair is a class that uses a little less memory then std::pair if one of its types is an empty class. Which allocator in this place can be. The compressed pair type is not part of the standard, but an implementation detail of the standard library, it works through using the empty base class optimization. Raymond Chen’s blog has a good explanation of this class.

While using compressed pair is an implementation detail, lets take a look how this works. Its an old trick, as if you have a class that contains no members, but you’d need to include it into the members of your type, then this will take up one byte of your type. As said a member of a type needs to be addressable, like when std::string calls allocate on its allocator member. One can get around this requirement with deriving from the empty class, now the derived type has naturally access to the needed member functions of its parent. As one may not want to do this to the actual std::string, a compressed pair provides this service. Also this gets around the problem when the allocator is not an empty class but does have some state with internal member variables. If such a type like compressed pair sounds handy to you, boost has an implementation. With C++20 there is an alternative to this trick with the attribute [[no_unique_address]], which achieves the same.

The actual implementation of std::string is a bit more complicated, and different in the various standard library implementations. This blog post gives a good overview on clangs libc++ implementation of std::string.

Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!