Skip to content
C++
Library
since C++98
Basic

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++98

std::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

cpp
#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

cpp
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 capacity

reserve() 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

cpp
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 copy

Always compare find results with std::string::npos, never with -1. find returns size_type (unsigned); comparing with -1 is undefined behaviour.

Number Conversions

cpp
// 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 involved

In 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:

ImplementationInline capacity
libstdc++ (GCC)15 chars
libc++ (Clang)22 chars (64-bit)
MSVC STL15 chars

sizeof(std::string) is 24 or 32 bytes (ABI-dependent); those bytes double as inline storage.

cpp
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 copy

The 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:

cpp
std::string result;
result.reserve(estimated_total);
for (const auto& chunk : chunks) {
    result += chunk;
    result += '\n';
}

Accept read-only strings as string_view:

cpp
// 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 params

Use std::format (C++20) for multi-part construction:

cpp
#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:

cpp
const char* p = s.c_str();
s += " more";   // may reallocate — p is now dangling, UB to dereference

operator+ chains allocate repeatedly:

cpp
// 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_chars for allocation-free numeric conversion (C++17)
  • std::ostringstream — stream-based string building (C++98)