C++ / a working model

57 / 163   ·   C++20   ·   10 min

string_view and span: lightweight borrows do not extend lifetime

Keep this sentence

string_view and span hand the caller an address and a range over contiguous data they do not own. They can cut copies and unify interfaces, but after the original object is destroyed, reallocated, or otherwise invalidated, the view is invalid as well. A read-only view still cannot escape lifetime rules.

In this lesson
  1. Separate owning data from describing data
  2. A const view object and const elements are two layers of const
  3. Range information is not automatic bounds protection
  4. Example
  5. Exercise

Separate owning data from describing data

C++17 string_view offers a read-only character-sequence interface, and C++20 span<T> describes a contiguous sequence of T. They do not free the elements, and copying a view does not copy the underlying data. They are appropriate for borrowing a slice of a string, array, or vector for the duration of a call, without forcing the caller to build a new container first.

That lightness has a definite cost: a view does not extend the owner's lifetime, and after the underlying storage is reallocated the old address is no longer usable. A function that returns a string_view into its own local string, or that lends a temporary vector's storage to a long-lived span, packages a lifetime bug as a seemingly modern interface. Ownership stays with the object that allocated the bytes; the view is only a description. If you cannot name the owner and show that it outlives every use of the view, you do not have a safe borrow.

A const view object and const elements are two layers of const

span<const int> forbids modifying elements through the view. const span<int> only forbids changing the view object's own binding; the int elements remain writable. string_view's character access is itself read-only, but an external owner may still modify the characters, so the view is not an immutable snapshot.

string_view also does not guarantee a terminating null. After you take a prefix, the bytes after data may be the rest of the original string rather than '\0'. Before passing the view to an interface that requires a C string, switch to an interface that takes an explicit length, or construct a truly owning, null-terminated string. A non-owning range is not a frozen copy: its meaning depends on storage that someone else must keep alive and, for C APIs, null-terminated.

Range information is not automatic bounds protection

A span may have a compile-time fixed extent or a run-time length. That range information makes the interface more complete than a raw pointer, but C++20's operator[] does not perform a standard-mandated throwing bounds check, and subspan's arguments must still satisfy preconditions. Callers and implementations still need to validate slice ranges.

The example builds a first-field view from a string that is still alive, then uses span to modify the tail of an array. Neither the owner nor the array is resized or destroyed around those operations, and the slice length is clearly valid. When results must cross threads, callbacks, or long-term caches, decide first who owns the storage. If the lifetime cannot be proved simply, copying into an owning object is usually more reasonable than hiding a dangling risk.

Pitfalls

  • string::substr returns a new string; you cannot assign that temporary to a string_view for the long term. Form a string_view first, then call its substr, to slice the original storage.
  • Vector reallocation, string operations that invalidate, or destruction of the owner all affect views. Copying a view and adding const cannot repair storage that has already died.

Run an example

Minimum C++20 · complete program · Download .cpp

#include <array>
#include <cassert>
#include <iostream>
#include <span>
#include <string>
#include <string_view>

std::string_view first_field(std::string_view text) {
    return text.substr(0, text.find(','));
}
void increment(std::span<int> values) {
    for (int& n : values) ++n;
}

int main() {
    std::string text = "alpha,beta";
    std::string_view field = first_field(text);
    assert(field == "alpha" && field.size() == 5);
    std::array<int, 3> values{1, 2, 3};
    increment(std::span<int>(values).subspan(1));
    std::span<const int> read_only(values);
    assert(read_only[0] == 1 && read_only[1] == 3 && read_only[2] == 4);
    std::cout << field << ' ' << read_only[1] << ' ' << read_only[2] << '\n';
}

Compile locally

g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread modern-views.cpp -o example && ./example

Expected result

alpha 3 4

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Why is auto field = first_field(std::string("alpha,beta")); not safe to print on the next statement? How should you keep the result?

Show a reference answer

The temporary string is destroyed at the end of the full expression that initializes field, so the returned view dangles immediately. Keep the string as a local owner that outlives field, or construct an owning result in the same full expression while the temporary still lives: std::string field(first_field(std::string("alpha,beta")));. The latter copies the characters and manages lifetime independently.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog