std::string::data vs c_str Safety in Modern C++
In modern C++, std::string::c_str() and
std::string::data() are frequently used interchangeably to
pass string data to C-style APIs, but their safety characteristics have
evolved across language revisions. While older standards differentiated
them by null-termination guarantees, modern C++ (C++11 through C++20 and
beyond) guarantees null-termination for both. Instead, the primary
safety differences in recent standards center on const-correctness,
mutable buffer access introduced in C++17, and the risk of corrupting
internal string invariants.
Null-Termination: The Historic Difference Resolved
Prior to C++11, c_str() guaranteed a null-terminated
character array, whereas data() was only required to return
a pointer to the internal array without a guaranteed null terminator at
the end. Calling data() and passing it to a function
expecting a C-style string was a common source of undefined
behavior.
Starting with C++11, the underlying storage of
std::string is strictly guaranteed to be contiguous, and
the character array returned by both c_str() and
data() is guaranteed to be null-terminated
(data()[size()] == '\0'). From a null-safety perspective,
both member functions are identical in all modern standards.
Const-Safety and the C++17 Non-Const Overload
The most significant safety divergence in recent standards was introduced in C++17.
c_str()remains strictly read-only:const CharT* c_str() const noexcept;data()gained a non-const overload alongside its const version:const CharT* data() const noexcept; CharT* data() noexcept; // Added in C++17
Because of this addition, data() provides direct write
access to the string's internal storage when called on a non-const
instance. This introduces two safety concerns:
- Accidental Mutation: Invoking
auto ptr = str.data();on a non-conststd::stringproduces achar*rather than aconst char*. This can bypass intended read-only consumption, allowing callers or downstream functions to unintentionally modify the string's content. - Type Deduction Risks: When passing
str.data()into generic template functions or auto-deduced parameters, the deduced type will be non-const if the original string object is non-const, expanding the interface contract beyond what might be intended.
Invariant Preservation and Undefined Behavior
When using the non-const data() overload to write
directly into a string's buffer (for example, using legacy C APIs like
snprintf or fread), the programmer is
responsible for maintaining the string's internal state.
- Null-Terminator Integrity: In modern C++,
str[str.size()]is valid to read (it returns'\0'), but modifying the null-terminator atdata()[size()]is undefined behavior prior to C++20, and strictly forbidden if not properly restored. - Size Desynchronization: Writing past the string's
size or writing fewer bytes than expected does not automatically update
str.size(). Reading functions that rely onstr.size()instead of scanning for\0will encounter out-of-sync or uninitialized data untilstr.resize()is explicitly invoked.
Because c_str() never yields a mutable pointer, it
prevents the caller from accidentally desynchronizing the size or
overwriting the terminating null byte.
Pointer Invalidation
Both data() and c_str() share identical
invalidation rules across all modern C++ standards. Pointers returned by
either function are invalidated whenever the string is modified in a way
that triggers reallocation (such as push_back(),
append(), resize(), or clear()).
However, because c_str() prevents in-place writes, it does
not obscure when a string mutation occurs, whereas holding a non-const
data() pointer can create aliasing bugs where the
underlying buffer is modified in one location while assumed static in
another.
Guidelines for Modern Codebases
- Use
c_str()when interfacing with C-style APIs that require a read-only, null-terminatedconst char*. It communicates intent clearly and prevents accidental modification of the string buffer. - Use
data()when read-write access to the internal contiguous buffer is explicitly required (e.g., passing to a C API that fills a buffer directly), or when interacting with APIs that operate on pointer-plus-size pairs (such asstd::string_view), taking care to handle the string's size explicitly.