C++ String Handling: std::string, string_view, and Performance Tips
Introduction: Strings in C++ — A Brief History
For decades, C++ developers worked with C-style strings: char* and char[]. They were fast (no overhead), but dangerous (easy to overflow, tedious to manage, error-prone).
// Old C-style way
char buffer[100];
strcpy(buffer, "Hello"); // Potential buffer overflow!
strcat(buffer, " World"); // No bounds checking
Then C++ introduced std::string (in the standard library), which brought safety and convenience:
// Modern C++ way
std::string str = "Hello";
str += " World"; // Safe, no buffer overflows
Recently, C++17 introduced std::string_view, which provides zero-copy string access for read-only operations.
This guide covers the modern C++ approach to strings—when to use each tool and how to write efficient, safe code.
std::string Fundamentals: Declaration, Initialisation, and Assignment
std::string is a dynamic string class that manages memory for you.
Creating Strings
#include <string>
std::string empty; // Empty string
std::string greeting = "Hello"; // Initialized from C-string literal
std::string copy = greeting; // Copy constructor
std::string moved = std::move(copy); // Move constructor (copy left empty)
std::string repeated(5, 'a'); // "aaaaa"
std::string from_range(str.begin(), str.end()); // From another range
String Size and Capacity
std::string str = "Hello";
str.length(); // Returns 5 (number of characters)
str.size(); // Same as length()
str.capacity(); // How much memory is allocated (usually > length)
str.empty(); // Returns true if length is 0
str.clear(); // Remove all characters
Memory management note: capacity() is often larger than length() because std::string allocates extra space to avoid reallocations when you append characters. This is called amortized O(1) growth.
Common std::string Operations
Appending and Concatenation
std::string str = "Hello";
str += " World"; // Append using +=
str.append(" World"); // Append using member function
str.push_back('!'); // Add single character
str.insert(5, " beautiful"); // Insert at position 5
Finding Substrings
std::string str = "The quick brown fox";
size_t pos = str.find("brown"); // Returns 10 (or npos if not found)
if (pos != std::string::npos) {
std::cout << "Found at position " << pos << std::endl;
}
size_t pos2 = str.rfind("o"); // Find from the right (returns 17)
Extracting Substrings
std::string str = "Hello World";
std::string sub = str.substr(0, 5); // "Hello"
std::string rest = str.substr(6); // "World" (from position 6 to end)
Replacing
std::string str = "hello world";
str.replace(0, 5, "goodbye"); // Replace first 5 chars with "goodbye"
// Result: "goodbye world"
Erasing
std::string str = "Hello World";
str.erase(5); // Remove from position 5 onwards: "Hello"
str.erase(0, 5); // Remove 5 characters starting at 0: " World"
Comparison
std::string a = "hello";
std::string b = "hello";
std::string c = "world";
a == b; // true
a != c; // true
a < c; // true (lexicographic comparison)
a.compare(b) == 0; // true (alternative comparison)
Accessing Characters
std::string str = "Hello";
char c = str[0]; // 'H' (no bounds checking)
char c2 = str.at(0); // 'H' (throws exception if out of bounds)
str[0] = 'J'; // Change first character
Converting Between Strings and Numbers
String to Number
std::string num_str = "42";
int num = std::stoi(num_str); // String to int
float f = std::stof("3.14"); // String to float
double d = std::stod("3.14159"); // String to double
long long big = std::stoll("999999999"); // String to long long
These functions throw std::invalid_argument if conversion fails.
Number to String
int num = 42;
double pi = 3.14159;
std::string s1 = std::to_string(num); // "42"
std::string s2 = std::to_string(pi); // "3.141590" (note: limited precision)
Note: std::to_string() has limited precision for floating-point numbers. For more control, use std::ostringstream (shown later).
C-Style Strings (char*) vs std::string: When and Why
Why std::string is Better
// C-style: manual management
char* cstr = new char[100];
strcpy(cstr, "Hello");
strcat(cstr, " World");
delete[] cstr; // Must remember to free!
// C++ style: automatic management
std::string str = "Hello";
str += " World"; // Automatically manages memory
// Memory freed automatically when str goes out of scope
Safety, convenience, and no memory leaks.
When to Use C-Strings
- Legacy code or C libraries: Some C functions expect
char*. Use.c_str()to convert:
std::string str = "filename.txt";
FILE* file = fopen(str.c_str(), "r"); // C-style file function
-
Performance-critical code where every nanosecond matters: C-strings have zero overhead. But generally,
std::string’s performance is excellent—don’t micro-optimize prematurely. -
Embedded systems with severe memory constraints: C-strings are predictable in size.
std::stringmight allocate more than needed due to growth strategy.
Best practice: Use std::string by default. Convert to char* only when interfacing with C libraries.
std::string_view (C++17): Zero-Copy String Access
std::string_view represents a non-owning, read-only view of a string. It doesn’t copy—just points to existing data.
Why string_view Matters
// Without string_view
void process(const std::string& str) {
// If str is a temporary, it's copied here
// If str is a C-string literal, it's copied to a temporary std::string
}
process("Hello"); // Wasteful: creates a temporary std::string just to read it
// With string_view
void process(std::string_view str) {
// No copy! Just reads the data where it is
}
process("Hello"); // Efficient: string_view points to the literal
Creating string_view Objects
std::string str = "Hello World";
std::string_view view(str); // View of a std::string
std::string_view literal_view("Hello"); // View of a string literal
view = "Another"; // Can reassign to point elsewhere
string_view Member Functions
std::string_view str = "Hello World";
str.size(); // 11
str.empty(); // false
str.find("World"); // 6
str.substr(0, 5); // "Hello" (returns another string_view)
When string_view is Dangerous
Lifetime issue:
std::string_view get_view() {
std::string local = "temporary";
return std::string_view(local); // DANGER! Points to destroyed string
}
std::string_view view = get_view();
std::cout << view; // Undefined behavior! local was destroyed
string_view is just a pointer—it doesn’t own the data. If the underlying string is destroyed, you have a dangling pointer.
Safe pattern:
void process(std::string_view str) {
// Use str here, where we know the original is still alive
}
std::string s = "Hello";
process(s); // Safe: s lives longer than the function call
Use string_view for function parameters when you’re only reading, not storing.
String Formatting in C++20: std::format
C++20 introduces std::format for type-safe string formatting (similar to Python’s f"{var}"):
#include <format>
int age = 30;
std::string name = "Alice";
std::string message = std::format("Hello, {}! You are {} years old.", name, age);
// Result: "Hello, Alice! You are 30 years old."
With precision control:
double pi = 3.14159265;
std::string s = std::format("Pi is approximately {:.2f}", pi);
// Result: "Pi is approximately 3.14"
This is cleaner and type-safe compared to printf() (which is error-prone) or std::ostringstream (which is verbose).
Efficient String Building: Why += in a Loop is Slow
Concatenating strings in a loop seems convenient but can be inefficient:
// SLOW
std::string result;
for (int i = 0; i < 1000000; ++i) {
result += "x"; // Reallocates and copies each iteration!
}
Why is this slow?
Each += might trigger a reallocation:
- Allocate new memory (larger capacity)
- Copy existing characters
- Append new character
- Deallocate old memory
After 1,000,000 iterations, you’ve reallocated millions of times.
Solution 1: Pre-allocate with reserve()
std::string result;
result.reserve(1000000); // Allocate space upfront
for (int i = 0; i < 1000000; ++i) {
result += "x"; // Now just appends, no reallocation
}
Much faster because we allocate once instead of many times.
Solution 2: Use std::stringstream
#include <sstream>
std::ostringstream oss;
for (int i = 0; i < 1000000; ++i) {
oss << "x"; // Internally optimized for building strings
}
std::string result = oss.str();
std::ostringstream is designed for incremental building.
Benchmark Comparison
#include <chrono>
// Slow: no pre-allocation
{
auto start = std::chrono::high_resolution_clock::now();
std::string s;
for (int i = 0; i < 1000000; ++i) {
s += "x";
}
auto end = std::chrono::high_resolution_clock::now();
// Likely 100+ milliseconds
}
// Fast: with reserve
{
auto start = std::chrono::high_resolution_clock::now();
std::string s;
s.reserve(1000000);
for (int i = 0; i < 1000000; ++i) {
s += "x";
}
auto end = std::chrono::high_resolution_clock::now();
// Likely 1-5 milliseconds (10-100x faster!)
}
std::stringstream for Complex String Construction
Use std::stringstream (or std::ostringstream) for building strings with mixed types:
#include <sstream>
std::ostringstream oss;
oss << "Name: " << "Alice" << std::endl;
oss << "Age: " << 30 << std::endl;
oss << "Score: " << 95.5 << std::endl;
std::string result = oss.str();
// Result: "Name: Alice\nAge: 30\nScore: 95.5\n"
Much cleaner than using + with std::to_string() repeatedly.
For parsing (extracting values from a string):
std::istringstream iss("42 3.14 hello");
int num;
double decimal;
std::string word;
iss >> num >> decimal >> word;
// num = 42, decimal = 3.14, word = "hello"
String Splitting and Joining
Splitting
C++ doesn’t have a built-in split, but it’s easy to implement:
#include <sstream>
#include <vector>
std::vector<std::string> split(const std::string& str, char delimiter) {
std::vector<std::string> tokens;
std::istringstream stream(str);
std::string token;
while (std::getline(stream, token, delimiter)) {
tokens.push_back(token);
}
return tokens;
}
int main() {
std::vector<std::string> parts = split("apple,banana,cherry", ',');
// parts = {"apple", "banana", "cherry"}
}
Joining
std::string join(const std::vector<std::string>& parts, const std::string& delimiter) {
if (parts.empty()) return "";
std::string result = parts[0];
for (size_t i = 1; i < parts.size(); ++i) {
result += delimiter + parts[i];
}
return result;
}
int main() {
std::vector<std::string> parts = {"apple", "banana", "cherry"};
std::string result = join(parts, ", "); // "apple, banana, cherry"
}
Or with std::ostringstream (more efficient):
std::string join(const std::vector<std::string>& parts, const std::string& delimiter) {
std::ostringstream oss;
for (size_t i = 0; i < parts.size(); ++i) {
if (i > 0) oss << delimiter;
oss << parts[i];
}
return oss.str();
}
Case Conversion, Trimming, and Common Utilities
Converting to Uppercase/Lowercase
#include <algorithm>
#include <cctype>
std::string to_upper(std::string str) {
std::transform(str.begin(), str.end(), str.begin(),
[](unsigned char c) { return std::toupper(c); });
return str;
}
std::string to_lower(std::string str) {
std::transform(str.begin(), str.end(), str.begin(),
[](unsigned char c) { return std::tolower(c); });
return str;
}
int main() {
std::string s = "Hello World";
std::cout << to_upper(s) << std::endl; // "HELLO WORLD"
std::cout << to_lower(s) << std::endl; // "hello world"
}
Trimming Whitespace
#include <algorithm>
#include <cctype>
std::string trim(std::string str) {
// Trim left
str.erase(str.begin(),
std::find_if(str.begin(), str.end(),
[](unsigned char c) { return !std::isspace(c); }));
// Trim right
str.erase(std::find_if(str.rbegin(), str.rend(),
[](unsigned char c) { return !std::isspace(c); }).base(),
str.end());
return str;
}
int main() {
std::string s = " Hello World ";
std::cout << "[" << trim(s) << "]" << std::endl; // "[Hello World]"
}
Unicode and C++ Strings: A Brief Introduction
The problem: C++ strings are sequences of bytes. Unicode characters are multi-byte (UTF-8, UTF-16, etc.).
std::string str = "Café"; // 5 bytes (é is multi-byte in UTF-8)
std::cout << str.size() << std::endl; // Prints 5, not 4!
The harsh truth: C++ doesn’t have built-in Unicode support. Proper Unicode handling requires third-party libraries like ICU (International Components for Unicode).
For most applications, just:
- Use UTF-8 encoding consistently
- Treat
std::stringas bytes, not characters - Use a library if you need proper Unicode support
// Work with UTF-8 bytes
std::string utf8_string = "Hello 世界"; // UTF-8 encoded
// Don't assume one character = one byte
Regular Expressions with std::regex
C++ has regex support via std::regex:
#include <regex>
std::string text = "My email is alice@example.com";
std::regex email_regex(R"(\w+@\w+\.\w+)");
if (std::regex_search(text, email_regex)) {
std::cout << "Email found!" << std::endl;
}
Extract matches:
std::smatch match;
if (std::regex_search(text, match, email_regex)) {
std::cout << "Email: " << match[0] << std::endl; // "alice@example.com"
}
Replace with regex:
std::string result = std::regex_replace(text, email_regex, "[REDACTED]");
// "My email is [REDACTED]"
Warning: std::regex is powerful but slow (NFA engine). For simple patterns, manual parsing or std::string::find() is faster.
Conclusion
Master string handling in C++:
- Use
std::stringby default for safety and convenience - Use
std::string_viewfor function parameters when reading, not storing - Pre-allocate with
.reserve()or usestd::ostringstreamfor efficient building - Use
std::format(C++20) for readable, type-safe formatting - Understand the byte-level nature of UTF-8 in C++
- Use regex carefully—it’s slower than direct string operations for simple tasks
Modern C++ (C++17 and beyond) provides excellent string tools. Mastering them leads to faster, safer code that’s a pleasure to maintain.
What string manipulation problem do you encounter most often? Share in the comments—we might cover it in the next guide!
Take Your C++ Further
If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.
👉 Get the C++ Better Explained Ebook — $19