A Zoo of Strings in Your C++ Code
C++ offers a bewildering variety of string types, from raw char* to std::string_view to game engine specializations like FString and StringID. This article uses animal metaphors to guide you through choosing the right string type for each situation.
If you've ever worked on a large C++ project, you've probably encountered a bewildering variety of string types. Raw pointers, standard library strings, framework-specific strings, hash-based identifiers — the list goes on. Let's take a tour through this zoo and understand when each "animal" is the right choice.
char* — The Wild Wolf
The most primitive string type: a pointer to a byte of data in memory. It's fast, lightweight, and dangerously unpredictable. There's no size tracking, no bounds checking, and no automatic memory management. One wrong move and you have a buffer overflow, a dangling pointer, or a use-after-free bug.
Despite its dangers, char* remains alive because it has virtually zero overhead and provides maximum control. It's the foundation everything else is built upon.
char[N] — The Turtle
Fixed-size stack-allocated buffers. They're simple, predictable, and centuries old in programming terms. You know exactly how much memory they use, they never allocate on the heap, and they're perfect for small, fixed-size data like file paths or configuration keys.
The downside is obvious: fixed size means wasted space or truncation. But in embedded systems and performance-critical code, the turtle still wins races.
String Literals — The Fish in a Glass
String literals live in the .rodata section of your binary. They exist for the entire program lifetime, require no memory management, and are completely immutable. They're the safest strings you can have — as long as you never try to modify them.
std::string — The Domestic Dog
std::string is the workhorse of C++ string handling. It provides RAII, automatic memory management, and a rich API. But convenience comes at a cost: heap allocations, potential iterator invalidation, and sometimes surprising performance characteristics.
The biggest trap is implicit conversions. When you pass a const char* to a function expecting const std::string&, a temporary string is created, triggering a heap allocation:
bool compare(const std::string& s1, const std::string& s2) {
return s1 == s2;
}
std::string str = "hello";
compare(str, "world"); // Creates a temporary std::string, allocation!
std::string_view — The Thin Cat
Introduced in C++17, std::string_view is a non-owning reference to a string. It eliminates unnecessary copies by simply pointing to existing data. It works with any string type — literals, std::string, char*:
std::string str = "this is my input string";
std::string_view sv(&str[11], 2); // "my" — no copying
void print(std::string_view sv) {
std::cout << sv;
}
print("literal"); // OK
print(std::string("str")); // OK
print(some_char_ptr); // OK
The danger: string_view doesn't own its data. If the underlying string is destroyed, your view becomes a dangling reference.
std::pmr::string — The Overeater
A polymorphic variant of std::string that uses memory resources, allowing flexible allocator strategies at runtime. Instead of being tied to a compile-time allocator, you can swap memory strategies through virtual functions:
class memory_resource {
public:
void* allocate(size_t bytes, size_t alignment);
void deallocate(void* ptr, size_t bytes, size_t alignment);
bool is_equal(const memory_resource& other) const;
private:
virtual void* do_allocate(size_t, size_t) = 0;
virtual void do_deallocate(void*, size_t, size_t) = 0;
virtual bool do_is_equal(const memory_resource&) const = 0;
};
namespace pmr {
using string = std::basic_string<char, std::char_traits<char>,
polymorphic_allocator<char>>;
}
QString — The Chatty Parrot
Qt's string class uses UTF-16 internally, which gives it rich Unicode support but creates compatibility headaches when interfacing with the rest of the C++ world, which mostly speaks UTF-8. It's feature-rich — regular expressions, splitting, formatting — but ties you firmly to the Qt ecosystem.
NSString — The Unique Zoo Specimen
Apple's string type from Objective-C. Immutable by design, with bidirectional CFStringRef casting. It lives in a completely separate ecosystem and requires toll-free bridging to interact with C++ code.
std::wstring — The Evolutionary Mistake
Wide-character strings sound good in theory but suffer from a fundamental flaw: wchar_t is 16 bits on Windows and 32 bits on Linux. This means std::wstring code that works on one platform may break on another. Converting between wide and narrow strings adds more confusion:
// UTF-8 string
std::string utf8 = u8"Hello, world! 🌍";
// Convert UTF-8 -> UTF-16 (std::wstring)
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
std::wstring wide = converter.from_bytes(utf8);
// Convert UTF-16 -> UTF-8
std::string utf8_again = converter.to_bytes(wide);
FrameString — The Mayfly
In game engines, some strings only live for a single frame. Using arena allocators, these strings are allocated from a pre-reserved block of memory and freed all at once when the frame ends — zero individual deallocations:
class ArenaAllocator {
char buffer[10 * 1024 * 1024]; // 10 MB
size_t offset = 0;
public:
char* alloc(size_t n) {
char* ptr = buffer + offset;
offset += n;
return ptr; // Instant!
}
void reset() { offset = 0; } // Reset in O(1)
};
String debugMsg(framemem_ptr);
debugMsg = "Player position: ";
debugMsg += to_string(x);
debugMsg += ", ";
debugMsg += to_string(y);
// At the end of the frame, all memory is automatically freed
FString — The Golden Retriever (Unreal Engine)
Unreal Engine's own string type, feature-rich and deeply integrated into the engine's ecosystem. It provides excellent tooling but makes your code entirely dependent on the Unreal runtime.
StringAtom — The Immortal Turtle
String interning stores each unique string exactly once in a global table and returns an ID or pointer. Subsequent comparisons become pointer equality checks — O(1) instead of O(n):
StringAtom healthTag("Health");
StringAtom manaTag("Mana");
// Comparison is instant — just ID or pointer equality
if (component.tag == healthTag) {
// ...
}
StringID — The Sprinting Cheetah
The ultimate optimization: convert strings to hash values at compile time. No storage, no table lookups — just a 32-bit integer. This even enables using strings in switch statements:
constexpr uint32_t operator""_sid(const char* str, size_t len) {
return FNV1a(str);
}
constexpr uint32_t damageEvent = "DamageEvent"_sid;
switch(messageType) {
case "PlayerDied"_sid:
handlePlayerDeath();
break;
case "EnemySpawned"_sid:
handleEnemySpawn();
break;
}
Conclusion
There is no single "best" string type in C++. Each has its niche: std::string for general use, string_view for zero-copy references, StringID for performance-critical comparisons, and framework-specific types when you're locked into an ecosystem. The key is to understand the trade-offs and choose the right tool for each job rather than forcing one solution everywhere.