std::string
std::string — owning mutable character sequence with SSO, full modification API, searching, numeric conversions, and C++17/20/23 additions.
std::stringsince C++98std::string is a specialisation of std::basic_string<char> — an owning, heap-backed, mutable sequence of char with value semantics, automatic memory management, and Small String Optimization that eliminates heap allocation for short strings.
Overview
std::string has been the standard text type since C++98. It owns its buffer, grows on demand, and provides amortised O(1) append. Any mutation that changes size or capacity invalidates all iterators and pointers into the string.
std::basic_string<CharT, Traits, Allocator> is the underlying template. Besides std::string (char), the standard provides std::u16string and std::u32string (C++11), std::wstring (C++98), and std::u8string (C++20).
For read-only string access, prefer std::string_view (C++17): it works with literals, existing buffers, and substrings without any allocation.
Construction
#include <string>
std::string s1; // empty — no heap touch
std::string s2 = "hello"; // copy from literal
std::string s3("hello", 3); // "hel" — first N bytes
std::string s4(5, 'x'); // "xxxxx"
std::string s5 = s2; // copy
std::string s6 = std::move(s2); // move — C++11; O(1) only for long strings (see SSO)
std::string s7(s5.begin(), s5.end()); // range constructor
// From std::string_view — C++17
std::string_view sv = "hello";
std::string s8{sv};
// User-defined literal suffix — C++14
using namespace std::string_literals;
auto s9 = "hello"s; // std::string, not const char*The "hello"s suffix matters whenever template argument deduction needs a concrete std::string type, and it enables "a"s + "b"s (which "a" + "b" cannot do).
Modification
std::string s = "Hello";
s += " World"; // append — amortised O(1)
s.append(" !"); // equivalent to +=
s.push_back('?'); // single char
s.insert(5, ","); // insert at byte position
s.erase(5, 1); // remove 1 char at pos 5
s.erase(s.begin() + 5); // iterator overload
s.replace(0, 5, "Hi"); // replace [pos, pos+len) with new content
s.clear(); // empty string — capacity unchanged
s.resize(10); // grows: pads with '\0'
s.resize(3); // shrinks: truncates
s.reserve(128); // pre-allocate — avoids reallocation in loops
s.shrink_to_fit(); // advisory release of excess capacityreserve() is the most impactful single optimisation for string building. If you can compute the final length upfront, one reserve eliminates all reallocation.
Access and Searching
std::string s = "Hello, World!";
s.size(); // 13 — O(1)
s.empty(); // false
s.capacity(); // ≥ size(), implementation-defined
s[0]; // 'H' — no bounds check, UB on bad index
s.at(0); // 'H' — throws std::out_of_range on bad index
s.front(); // 'H'
s.back(); // '!'
s.c_str(); // const char* — null-terminated, valid until next mutation
s.data(); // const char* (C++11); writable char* for non-const string (C++17)
// find returns index or std::string::npos on miss
s.find("World"); // 7
s.find('o'); // 4
s.rfind('o'); // 8 — last occurrence
s.find("l", 4); // 10 — start search at offset 4
s.find_first_of("aeiouAEIOU"); // 1 — index of first vowel
s.find_last_of("aeiouAEIOU"); // 8
s.find_first_not_of("Helo, W"); // 9 — 'r'
s.starts_with("Hello"); // true — C++20
s.ends_with("!"); // true — C++20
s.contains("World"); // true — C++23
s.substr(7); // "World!" — allocates a new std::string
s.substr(7, 5); // "World"
// Slice without allocation — C++17
std::string_view sv{s};
sv.substr(7, 5); // returns a string_view, zero copyAlways compare find results with std::string::npos, never with -1. find returns size_type (unsigned); comparing with -1 is undefined behaviour.
Number Conversions
// Numeric → string (always allocates)
std::string s1 = std::to_string(42); // "42"
std::string s2 = std::to_string(3.14); // "3.140000"
// String → numeric (throw std::invalid_argument or std::out_of_range on failure)
int i = std::stoi("42");
long l = std::stol("1234567");
double d = std::stod("2.718281828");
int hex = std::stoi("FF", nullptr, 16); // 255
// C++17: from_chars / to_chars — no allocation, no locale, no exceptions
#include <charconv>
int n{};
auto [ptr, ec] = std::from_chars(s1.data(), s1.data() + s1.size(), n);
if (ec == std::errc{}) { /* n is valid */ }
char buf[32];
auto [end, ec2] = std::to_chars(buf, buf + sizeof(buf), 42);
std::string_view result{buf, end}; // no heap involvedIn performance-sensitive code, from_chars/to_chars are the correct tools: locale-independent, exception-free, and zero-allocation.
Small String Optimization (SSO)
Short strings are stored directly inside the std::string object, avoiding any heap allocation:
| Implementation | Inline capacity |
|---|---|
| libstdc++ (GCC) | 15 chars |
| libc++ (Clang) | 22 chars (64-bit) |
| MSVC STL | 15 chars |
sizeof(std::string) is 24 or 32 bytes (ABI-dependent); those bytes double as inline storage.
std::string a = "Hi!"; // fits inline — zero malloc
std::string b = std::move(a); // copies the buffer, NOT O(1)
// 'a' is valid-but-unspecified after move; do not rely on it being empty
std::string c = "This string is long enough to require a heap allocation";
std::string d = std::move(c); // O(1) — pointer swap, no copyThe exact SSO threshold is ABI-specific, not standardised. Never hard-code the threshold value in production logic; measure with your toolchain.
Best Practices
Pre-reserve before accumulation loops:
std::string result;
result.reserve(estimated_total);
for (const auto& chunk : chunks) {
result += chunk;
result += '\n';
}Accept read-only strings as string_view:
// Accepts literals, std::string, std::string_view, buffers — zero copy
void log(std::string_view msg);
// Forces an allocation when caller passes a literal
void log(const std::string& msg); // avoid for read-only paramsUse std::format (C++20) for multi-part construction:
#include <format>
std::string msg = std::format("{}:{} — {}", file, line, reason);
// Single allocation; cleaner than chained operator+Use from_chars/to_chars in hot paths instead of stoi/to_string to eliminate locale overhead and allocations.
Common Pitfalls
c_str() and data() pointers go stale on mutation:
const char* p = s.c_str();
s += " more"; // may reallocate — p is now dangling, UB to dereferenceoperator+ chains allocate repeatedly:
// 3 temporary strings and 3 allocations
std::string result = a + ", " + b + "!";
// 1 allocation, 4 appends
std::string result;
result.reserve(a.size() + 2 + b.size() + 1);
result += a; result += ", "; result += b; result += '!';substr in hot loops creates heap allocations: use std::string_view::substr for intermediate slicing, then construct a std::string only when ownership is required.
Moving a short string is not O(1): SSO strings carry their data inline; the move constructor must copy bytes. Profile before assuming move is free.
Comparison with std::string::npos: find returns std::string::size_type (unsigned). Testing against != -1 silently converts -1 to npos on most platforms but is technically implementation-defined; always write != std::string::npos.
See Also
std::string_view— non-owning, read-only string reference (C++17)std::format— type-safe formatted string construction (C++20)<charconv>—from_chars/to_charsfor allocation-free numeric conversion (C++17)std::ostringstream— stream-based string building (C++98)