Skip to content
C++ Better Explained
Go back

C++ String Handling: std::string, string_view, and Performance Tips

Edit page

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

  1. 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
  1. 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.

  2. Embedded systems with severe memory constraints: C-strings are predictable in size. std::string might 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:

  1. Allocate new memory (larger capacity)
  2. Copy existing characters
  3. Append new character
  4. 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:

  1. Use UTF-8 encoding consistently
  2. Treat std::string as bytes, not characters
  3. 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++:

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



Edit page
Share this post on:

Previous Post
C++ Templates From Scratch: Generic Programming Explained Simply
Next Post
Debugging C++ with GDB: A Practical Step-by-Step Guide