Search⌘ K
AI Features

Iterators, Strings, and String Views

Explore how iterators help traverse different containers uniformly, ensuring flexible and maintainable code. Understand the distinction between std::string for owning text and std::string_view for efficient, non-owning access, mastering safe and performant string handling in modern C++.

We have learned how to store data in containers like std::vector and std::array. However, relying on index loops (e.g., i = 0 to size) has a major flaw: it only works for containers that support indexing. If we later decide to switch our container to a std::list or std::set (which do not allow [] access), we would have to rewrite every single loop in our program.

To solve this, C++ provides iterators. Iterators give us a universal way to traverse data, regardless of how it is stored in memory. Additionally, since text is the most common data we handle, we will master the two essential tools for it: std::string for ownership and modification, and std::string_view for high-performance reading.

Understanding iterators

An iterator is a generalized pointer that abstracts the "position" of an element. It acts as a cursor. By using iterators, we write loops that work identically whether the underlying data is a contiguous array or a linked structure.

Standard containers provide two standard anchor points:

  • begin(): Points to the first element.

  • end(): Points to the position one step past the last element. It is a sentinel value (a "stop" sign) and does not point to valid data.

The syntax std::vector<int>::iterator can look intimidating. Let's break it down:

  1. std::vector<int>: This is the specific container type we are working with.

  2. ::: The scope resolution operator tells the compiler to look inside that class.

  3. iterator: This is a nested type defined specifically for vectors. It behaves like a pointer that knows how to navigate a vector's memory.

Here is how we use this type to build a loop that doesn't depend on integer indexes:

C++ 23
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {10, 20, 30};
// 'it' is an iterator for a vector of integers
std::vector<int>::iterator it = scores.begin();
// Loop until we hit the "stop sign" (end)
while (it != scores.end()) {
std::cout << *it << " "; // Dereference to get the value
++it; // Move to the next element
}
std::cout << "\n";
return 0;
}

Let’s break this down step by step:

  • Line 8: We create an iterator that represents a position inside the vector. Instead of working with indexes, iterators let us traverse a container in a way that works consistently across many different container types.

  • Line 11: This comparison shows how iteration is controlled. scores.end() acts as a sentinel value that marks one position past the last element, allowing the loop to know when to stop safely.

  • Line 12: Dereferencing the iterator gives access to the element at the current position. This highlights the key idea that iterators behave like pointers to elements inside a container.

  • Line 13: Advancing the iterator moves the position forward through the container. This demonstrates how iterators provide sequential access without exposing how the container stores its elements internally.

Const correctness and safety

Often, we only want to read data, not change it. Using a regular iterator is risky because it allows modification (e.g., *it = 0 would be valid). To prevent accidental bugs, C++ provides const_iterator.

A const_iterator works exactly like a regular iterator, but it forbids writing to the element it points to. We obtain one using .cbegin() (const begin) and .cend() (const end).

Can we write std::vector<int>::const_iterator explicitly? Absolutely. In fact, doing so ensures the compiler enforces our read-only intent.

C++ 23
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {10, 20, 30};
// Explicitly using const_iterator enforces read-only access
std::vector<int>::const_iterator it = scores.cbegin();
while (it != scores.cend()) {
std::cout << *it << " ";
// *it = 50; // Compilation Error! logic implies we cannot write here
++it;
}
return 0;
}

Let’s break this down step by step:

  • Line 8: We initialize it using cbegin(). The type const_iterator means *it returns a constant reference.

  • Line 13: If we tried to assign a new value to *it, the compiler would generate an error because the iterator promises not to modify the container. This makes our code's intent clear and strictly enforced.

Managing text with std::string

Text handling requires safety. In C++, std::string manages a dynamic array of characters for us. We can also extract parts of a string using functions like substr(). It grows when we add text and cleans up memory when we are done.

A critical decision when using strings is how we access individual characters. We have two options:

  1. [] (subscript operator): This is fast because it does not check if the index is valid. If we ask for str[100] on a short string, the program will likely crash or behave unpredictably (undefined behavior).

  2. .at() (Method): This is safe. It checks the bounds before access. If the index is invalid, it throws a standard exception, allowing us to handle the error gracefully.

Let's see this safety difference in action:

C++ 23
#include <iostream>
#include <string>
#include <stdexcept> // For std::out_of_range
int main() {
std::string text = "C++";
// 1. Modifying strings
text += " Rocks"; // text is now "C++ Rocks"
try {
// Safe access: We access index 0 ('C')
std::cout << "First: " << text.at(0) << "\n";
// Unsafe access caught: Index 100 doesn't exist
std::cout << "Invalid: " << text.at(100) << "\n";
}
catch (const std::out_of_range& e) {
// The .at() method detected the error and sent us here
std::cerr << "Caught error: " << e.what() << "\n";
}
return 0;
}

Let’s break this down step by step:

  • Line 9: We use += to append text. The std::string object automatically resizes its internal memory to fit the new characters.

  • Line 13: .at(0) validates that index 0 is within the string's size (it is), then returns the character C.

  • Line 16: .at(100) validates that index 100 is outside the string boundaries. Instead of crashing efficiently, it throws an exception, protecting the program from corruption.

  • Line 18: The program control jumps here safely, proving that .at() prevented undefined behavior.

Extracting part of a string with substr()

C++ 23
#include <iostream>
#include <string>
int main() {
std::string text = "Modern C++";
std::string part = text.substr(0, 6); // Extract "Modern"
std::cout << part << "\n";
return 0;
}

The substr(start, length) function returns a portion of the string starting at a given index.

Efficient text access with std::string_view

When we pass a std::string to a function by value, the computer must allocate new memory and copy every character. This is inefficient for read-only tasks.

C++17 solved this with std::string_view. Think of a view as a lightweight window looking at someone else's data. It consists only of a pointer (address of the text) and a size (how long the text is). It owns nothing and copies nothing.

This allows us to write functions that accept raw string literals (like "Hello") and std::string objects equally efficiently.

C++ 23
#include <iostream>
#include <string>
#include <string_view>
// This function takes a 'view' of the string, not a copy
void printFirstThree(std::string_view text) {
if (text.length() >= 3) {
// .substr on a view creates a new view, NOT a new string allocation
std::cout << "Prefix: " << text.substr(0, 3) << "\n";
}
}
int main() {
std::string s = "Modern C++";
// Case 1: Viewing a std::string
printFirstThree(s);
// Case 2: Viewing a string literal
printFirstThree("Performance");
return 0;
}

Let’s understand this step by step:

  • Line 6: The parameter std::string_view is tiny. It doesn't copy the text "Modern C++"; it just remembers where that text starts and ends.

  • Line 8: text.substr(0, 3) creates a new view of the string containing just the first three characters. This involves zero memory allocation, since it only adjusts the pointer and size.

  • Line 16: When we pass s, the view looks at s's internal data.

  • Line 19: When we pass "Performance", the view looks directly at the literal stored in the binary. This avoids creating a temporary std::string entirely.

Lifetime and safety risks

Because std::string_view is just a "window," the "house" (the actual data) must remain standing while we are looking at it.

A dangling view happens when the data is destroyed, but the view is still trying to look at it. This often occurs if we return a view to a variable that is about to be destroyed (like a local variable in a function). When the function ends, the local variable is wiped from memory (popped off the stack). The view returned to the caller now points to "garbage" memory.

C++ 23
#include <iostream>
#include <string>
#include <string_view>
std::string_view createBadView() {
std::string message = "I am temporary";
// DANGER: We are returning a view to 'message'
return message;
} // 'message' is destroyed here. The view now points to nothing
int main() {
// This view is dead on arrival
std::string_view view = createBadView();
// Accessing it is Undefined Behavior (crash or garbage output)
// std::cout << view << "\n";
return 0;
}

Let’s break this down step by step:

  • Line 6: message is a local variable. It lives only as long as createBadView is running.

  • Line 9: We return a view pointing to message.

  • Line 10: The closing brace is reached. message is destroyed and its memory reclaimed.

  • Line 14: view in main now holds the address of memory that no longer belongs to us. Accessing it is a critical bug.

We have now unlocked the ability to traverse any container uniformly using iterators, ensuring our code remains flexible even if we change container types later. We also distinguished between owning text with std::string and efficiently viewing it with std::string_view. We use std::string when we need to store data and std::string_view when we just need to read it. Mastering these tools allows us to write C++ that is both safe and high-performance.