Home C / C++ Mastering std::span and std::string_view in Modern C++
Intermediate 3 min · July 13, 2026

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.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of C++ (pointers, references, STL containers)
  • Familiarity with C++17/20 features
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is std?

std::span and std::string_view are non-owning views over contiguous sequences of objects and characters, respectively, allowing you to pass data without copying.

Imagine you have a library book (data) and you want to let someone read a specific chapter without photocopying the whole book.
Plain-English First

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.

string_view_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <string_view>

void print_length(std::string_view sv) {
    std::cout << "Length: " << sv.size() << '\n';
}

int main() {
    std::string s = "Hello, World!";
    const char* cstr = "C-string";
    
    print_length(s);          // from std::string
    print_length(cstr);       // from const char*
    print_length("Literal"); // from string literal
    
    std::string_view sub = s.substr(0, 5); // view of first 5 chars
    std::cout << sub << '\n'; // prints "Hello"
    return 0;
}
Output
Length: 13
Length: 8
Length: 7
Hello
⚠ Lifetime Danger
📊 Production Insight
In logging or serialization, using std::string_view can reduce allocations by 90%.
🎯 Key Takeaway
std::string_view is a lightweight, non-owning view that avoids copies. Use it for read-only string parameters.

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.

span_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <span>
#include <vector>

void print_sum(std::span<const int> sp) {
    int sum = 0;
    for (int x : sp) sum += x;
    std::cout << "Sum: " << sum << '\n';
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> vec = {10, 20, 30};
    
    print_sum(arr);       // from C-array
    print_sum(vec);       // from std::vector
    print_sum({1,2,3});   // from initializer list
    
    std::span<int> sp(arr);
    auto first3 = sp.first(3); // subspan
    for (int x : first3) std::cout << x << ' ';
    return 0;
}
Output
Sum: 15
Sum: 60
Sum: 6
1 2 3
💡Dynamic Extent vs Fixed Extent
📊 Production Insight
When refactoring legacy code that uses pointer+size pairs, replace them with std::span for safety and readability.
🎯 Key Takeaway
std::span is a universal interface for contiguous sequences. Use it to write container-agnostic functions.

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.

best_practices.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <string_view>
#include <span>
#include <algorithm>

// Good: accepts any string-like type
void to_upper(std::span<char> sp) {
    for (char& c : sp) c = std::toupper(c);
}

// Good: read-only view
void print(std::string_view sv) {
    std::cout << sv << '\n';
}

int main() {
    char arr[] = "hello";
    to_upper(arr);
    print(arr); // prints "HELLO"
    
    std::string s = "world";
    to_upper(s);
    print(s); // prints "WORLD"
    return 0;
}
Output
HELLO
WORLD
🔥Const Correctness
📊 Production Insight
In high-performance code, using std::span can reduce cache misses by allowing the compiler to optimize access patterns.
🎯 Key Takeaway
Prefer std::span and std::string_view in function interfaces to avoid template bloat and unnecessary copies.

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.

lifetime_danger.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string_view>

std::string_view get_subview() {
    std::string s = "temporary";
    return s; // DANGER: s is destroyed, view dangles
}

int main() {
    std::string_view sv = get_subview();
    std::cout << sv << '\n'; // undefined behavior
    return 0;
}
Output
Undefined behavior (likely garbage or crash)
⚠ Never Return a View to a Local
📊 Production Insight
In large codebases, consider using a wrapper class that explicitly documents lifetime requirements.
🎯 Key Takeaway
Always ensure the data source outlives the view. Use static analysis tools to catch lifetime issues.

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.

performance.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string_view>
#include <chrono>

int main() {
    const char* large = "..."; // imagine a large string
    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        std::string_view sv(large); // calls strlen each time
    }
    auto end = std::chrono::steady_clock::now();
    std::cout << "Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count() << "ms\n";
    return 0;
}
Output
Time: 1234ms (example)
💡Avoid Repeated strlen
📊 Production Insight
In hot paths, precompute lengths or use std::string to avoid repeated strlen.
🎯 Key Takeaway
Views are fast, but be mindful of hidden strlen calls. Use the two-argument constructor when possible.

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.

interop.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <span>
#include <cstring>

// C-style function that expects a buffer and size
void process_buffer(const char* data, size_t size) {
    std::cout << "Processing " << size << " bytes\n";
}

void modern_process(std::string_view sv) {
    // Safe: pass data and size to C function
    process_buffer(sv.data(), sv.size());
}

int main() {
    std::string s = "Hello";
    modern_process(s);
    
    // Construct span from C array
    int arr[] = {1,2,3};
    std::span<int> sp(arr);
    // Pass to C function expecting int* and size
    some_c_function(sp.data(), sp.size());
    return 0;
}
Output
Processing 5 bytes
⚠ Null Termination
📊 Production Insight
When wrapping C libraries, use std::span and std::string_view in your C++ interface to provide type safety and bounds checking.
🎯 Key Takeaway
Use .data() and .size() to interface with C APIs. Be cautious about null termination.
● Production incidentPOST-MORTEMseverity: high

The Slow Logging System: A Tale of Unnecessary Copies

Symptom
Logging was extremely slow, causing request timeouts in a high-throughput server.
Assumption
The developer assumed string copying was cheap and necessary for thread safety.
Root cause
Every log call created a deep copy of the log message string, leading to massive heap allocations and contention.
Fix
Replaced std::string parameters with std::string_view, eliminating copies. Used a thread-local buffer for formatting.
Key lesson
  • 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.
Production debug guideSymptom to Action3 entries
Symptom · 01
Crash or garbage data when using std::string_view
Fix
Check that the underlying string is still alive (dangling view). Use address sanitizer.
Symptom · 02
std::span out-of-bounds access
Fix
Enable bounds checking with gcc -D_GLIBCXX_DEBUG or use at() method.
Symptom · 03
Performance regression after switching to std::span
Fix
Ensure you're not accidentally copying the span itself (it's cheap, but passing by value is fine). Check for unintended conversions.
★ Quick Debug Cheat SheetCommon issues and immediate fixes for std::span and std::string_view.
Dangling view
Immediate action
Ensure the original data outlives the view.
Commands
g++ -fsanitize=address -g
valgrind --tool=memcheck
Fix now
Use std::string_view as function parameter, not as a class member that outlives the source.
Out-of-bounds access+
Immediate action
Use .at() instead of operator[]
Commands
g++ -D_GLIBCXX_DEBUG -g
std::span<int> s(data, size); s.at(100);
Fix now
Check size before access.
Unexpected null terminator assumption+
Immediate action
Do not assume std::string_view is null-terminated.
Commands
std::string_view sv = ...; if (sv.data()[sv.size()] != '\0') { /* handle */ }
std::string str(sv); // safe copy
Fix now
Use .data() only if you know it's null-terminated, or copy to std::string.
Featurestd::spanstd::string_view
Introduced inC++20C++17
Element typeAny type Tchar (or char-like)
Null-terminatedNoNo
Mutable accessYes (if T non-const)No
String operationsNoYes (find, substr, etc.)
Common useArray/vector viewsString views
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
string_view_example.cppvoid print_length(std::string_view sv) {What is std
span_example.cppvoid print_sum(std::span sp) {What is std
best_practices.cppvoid to_upper(std::span sp) {Common Use Cases and Best Practices
lifetime_danger.cppstd::string_view get_subview() {Lifetime and Safety Considerations
performance.cppint main() {Performance Implications
interop.cppvoid process_buffer(const char* data, size_t size) {Interoperability with Legacy Code and C APIs

Key takeaways

1
std::span and std::string_view are non-owning views that avoid copying data, improving performance and reducing allocations.
2
Use them as function parameters to accept various data sources without templates or overloading.
3
Always ensure the underlying data outlives the view to avoid dangling references.
4
Be aware that std::string_view is not null-terminated; copy to std::string if needed.
5
Enable debug iterators and use sanitizers to catch out-of-bounds access and lifetime issues.

Common mistakes to avoid

3 patterns
×

Returning 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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is std::string_view and when would you use it?
Q02SENIOR
Explain the lifetime issues with std::span and std::string_view.
Q03SENIOR
How would you implement a function that prints the first N elements of a...
Q04SENIOR
What are the performance implications of using std::string_view vs const...
Q01 of 04JUNIOR

What is std::string_view and when would you use it?

ANSWER
std::string_view is a non-owning view of a string. Use it as a function parameter to accept std::string, const char*, or string literals without copying. Avoid using it as a return value unless the underlying data outlives the function.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can I modify the underlying data through a std::string_view?
02
Is std::span always better than passing a vector reference?
03
How do I convert a std::string_view to a std::string?
04
What is the difference between std::span and std::string_view?
05
Can I use std::span with a dynamic array allocated with new?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's C++ Advanced. Mark it forged?

3 min read · try the examples if you haven't

Previous
C++23 std::expected and std::optional Monadic Operations
25 / 41 · C++ Advanced
Next
C++20/23 Chrono: Time Zones, Calendars, and Duration