Mastering std::span and std::string_view in Modern C++
Learn how to use std::span and std::string_view to write safer, faster C++ code.
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
- ✓Basic knowledge of C++ (pointers, references, STL containers)
- ✓Familiarity with C++17/20 features
- std::span is a non-owning view over a contiguous sequence of objects (like arrays or vectors).
- std::string_view is a non-owning view over a string (characters).
- Both avoid copying data, improving performance and reducing memory allocations.
- They provide bounds-checked access and work with C-style arrays and STL containers.
- Use them as function parameters to accept various data sources without templates.
Imagine you have a library book (data) and you want to let someone read a specific chapter without photocopying the whole book. std::span and std::string_view are like bookmarks: they point to the chapter (data) without owning it. This saves time and paper (memory).
In modern C++, performance and safety are paramount. Two powerful tools that help achieve both are std::span (C++20) and std::string_view (C++17). These non-owning views allow you to pass around references to contiguous data without copying, reducing allocations and improving cache locality. They also provide a uniform interface for arrays, std::vector, std::string, and even raw pointers with a size. This tutorial will dive deep into their usage, common pitfalls, and production-ready patterns. By the end, you'll be able to refactor your code to be more efficient and expressive, avoiding unnecessary copies and buffer overruns.
What is std::string_view?
std::string_view, introduced in C++17, is a non-owning reference to a contiguous sequence of characters. It typically consists of a pointer to the first character and a size. It does not allocate memory and does not manage the lifetime of the underlying string. Use it to pass string data to functions without copying, especially when you only need to read the data. It works with std::string, const char*, and even substrings. However, be cautious: std::string_view is not null-terminated, so functions expecting a C-string may misbehave. Also, ensure the underlying data outlives the view.
What is std::span?
std::span, introduced in C++20, is a non-owning view over a contiguous sequence of objects. It is essentially a pointer and a size, but with a rich interface similar to std::vector or std::array. It can be constructed from arrays, std::vector, std::array, or raw pointer+size. std::span provides bounds-checked access via .at() and supports iteration, subviews, and more. It is ideal for writing functions that operate on contiguous data without being templated on the container type. However, like std::string_view, it does not own the data, so lifetime management is critical.
Common Use Cases and Best Practices
Both std::span and std::string_view shine in function parameters. Instead of overloading for std::string, const char*, and std::string_view, just use std::string_view. Similarly, for arrays, use std::span. They also enable efficient substring and subarray operations without copying. Best practices: pass by value (they are cheap to copy), use const whenever possible, and never store them as class members unless you are certain the underlying data outlives the object. For APIs that need to modify data, consider std::span with mutable elements.
Lifetime and Safety Considerations
The most critical aspect of using views is ensuring the underlying data remains valid. A common mistake is returning a std::string_view from a function that creates a temporary std::string. Similarly, storing a std::span that points to a local array is dangerous. Always think about ownership. Use tools like AddressSanitizer and Valgrind to detect dangling references. Additionally, std::string_view is not null-terminated; if you need a C-string, copy to std::string first. For std::span, be aware that the size is not checked by default; use .at() for bounds checking or enable debug iterators.
Performance Implications
std::span and std::string_view are zero-overhead abstractions: they are essentially a pointer and a size. Passing them by value is cheap (two registers or stack slots). They eliminate copies of the underlying data, which reduces allocations and memory bandwidth. However, they can introduce subtle performance issues if used incorrectly. For example, iterating over a std::span may be slower than a raw pointer if the compiler cannot optimize away bounds checks (in debug mode). Also, constructing a std::string_view from a std::string is O(1), but constructing from a const char* requires a call to strlen (O(n)). To avoid that, use the two-argument constructor if you know the length.
Interoperability with Legacy Code and C APIs
Many C APIs expect a pointer and a size. std::span and std::string_view can bridge the gap: use .data() and .size() to pass to C functions. However, be careful: std::string_view.data() is not guaranteed to be null-terminated, so if the C function expects a null-terminated string, you must copy to a std::string or ensure the view is from a null-terminated source. For std::span, .data() gives a pointer to the first element. You can also construct a std::span from a pointer and size returned by a C API. This makes views excellent for wrapping legacy interfaces safely.
The Slow Logging System: A Tale of Unnecessary Copies
- Always pass string parameters by std::string_view when you don't need ownership.
- Profile before optimizing, but be aware of hidden copies.
- std::string_view is not null-terminated; ensure your functions handle that.
- Use std::span for array-like data to avoid pointer+size pairs.
at() method.g++ -fsanitize=address -gvalgrind --tool=memcheck| File | Command / Code | Purpose |
|---|---|---|
| string_view_example.cpp | void print_length(std::string_view sv) { | What is std |
| span_example.cpp | void print_sum(std::span | What is std |
| best_practices.cpp | void to_upper(std::span | Common Use Cases and Best Practices |
| lifetime_danger.cpp | std::string_view get_subview() { | Lifetime and Safety Considerations |
| performance.cpp | int main() { | Performance Implications |
| interop.cpp | void process_buffer(const char* data, size_t size) { | Interoperability with Legacy Code and C APIs |
Key takeaways
Common mistakes to avoid
3 patternsReturning a std::string_view from a function that creates a local std::string.
Assuming std::string_view is null-terminated.
Using std::span with a dynamic extent but passing a fixed-size array without specifying size.
Interview Questions on This Topic
What is std::string_view and when would you use it?
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
That's C++ Advanced. Mark it forged?
3 min read · try the examples if you haven't