AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

05 Strings / Lesson 6 of 6

std::string_view — a window onto text

13 min · +10 XP · 2 exercises · 6 questions

Read the text13 min
Exercises0/2
Quiz6 q

Quick answer

What is std::string_view in C++?

std::string_view (C++17) is a non-owning, read-only view of characters that someone else owns: just a pointer and a length, sixteen bytes on x86-64 and cheap to copy. It supports the read-only string interface, including find and an O(1) substr, and accepts literals and std::string without copying. It must never outlive, or survive a modification of, the text it points at.

Every std::string you pass, slice or compare costs a copy somewhere: a function taking const std::string& allocates when called with a literal, substr copies what it returns, and a parser that cuts a line into fields copies the whole line again in pieces. std::string_view (C++17) answers all three: a pointer and a length that refer to characters someone else owns. It is sixteen bytes on this platform, trivially copyable, its substr costs nothing — and it brings the one lifetime rule this lesson exists to make you respect.

What it is

#include <string_view>

std::string owner = "codekairo";
std::string_view whole = owner;              // implicit from std::string: points at owner's buffer
std::string_view lit = "literal";            // from a literal: strlen once, no copy
std::string_view part(owner.data() + 4, 5);  // pointer + length: "kairo"
std::cout << whole.size() << ' ' << part << ' ' << lit.substr(0, 3) << '\n';   // 9 kairo lit

A view holds data() and size() and nothing else — no buffer, no ownership, no null terminator of its own; copying one copies two words. It supports the whole read-only string interface: size, empty, [], at, front, back, iteration, the find family, substr, starts_with/ends_with, compare and all six comparison operators, which compare content just as std::string does. Anything that would change the characters — +=, push_back, insert, replace — does not exist, because the view does not own them. Two operations exist only on views: remove_prefix(n) and remove_suffix(n) shrink the window from either end in O(1), and are how a view is trimmed.

std::string_view v = "   padded   ";
v.remove_prefix(v.find_first_not_of(' '));                  // "padded   "
v.remove_suffix(v.size() - v.find_last_not_of(' ') - 1);    // "padded"

substr in O(1)

std::string line = "2026-09-17T10:15:00";
std::string_view sv = line;
std::string_view date = sv.substr(0, 10);     // "2026-09-17": a narrower window, no allocation
std::string_view time = sv.substr(11);        // "10:15:00"

std::string::substr allocates and copies; std::string_view::substr adjusts a pointer and a length in constant time. A tokeniser written with views allocates nothing until you decide to keep something:

#include <vector>

std::vector<std::string_view> words(std::string_view text) {
    std::vector<std::string_view> out;
    std::size_t i = 0;
    while (i < text.size()) {
        while (i < text.size() && text[i] == ' ') ++i;         // skip separators
        const std::size_t start = i;
        while (i < text.size() && text[i] != ' ') ++i;         // run to the end of the word
        if (start < i) out.push_back(text.substr(start, i - start));
    }
    return out;
}

Every element of out points into text's characters. That is the whole point, and the whole danger.

The right parameter type for read-only text

ParameterCalled with a literalCalled with a std::stringKnows its lengthNull-terminated
const char*fineneeds .c_str()no — strlenyes
const std::string&constructs a temporary std::string (allocates past 15 chars)fine, no copyyesyes
std::string_viewfine, no copyfine, no copyyesnot guaranteed

Take std::string_view by value — two words; a reference would be an indirection for nothing — whenever the function only reads text: counting, searching, parsing, printing. The caller passes a literal, a std::string, another view or a (pointer, length) pair and nothing is copied. Return a std::string, not a view, when the function produces text.

int count_char(std::string_view text, char c) {
    int n = 0;
    for (char ch : text) if (ch == c) ++n;
    return n;
}

count_char("banana", 'a');          // 3 - no std::string was ever built
count_char(owner, 'k');             // 1 - no copy of owner

The lifetime rule

A view is valid exactly as long as the characters it points at. Nothing in the language checks this, and the failures are undefined behaviour that often appears to work:

std::string_view bad1 = std::string("temporary");     // the string dies at the ';' - dangling immediately
std::string_view bad2 = owner + "!";                  // same: + returns a temporary

std::string_view first_word() {
    std::string line;
    std::getline(std::cin, line);
    return std::string_view(line).substr(0, line.find(' '));   // line is destroyed on return
}

std::vector<std::string> names = {"ada"};
std::string_view n0 = names[0];
names.push_back("grace");                             // may reallocate: n0 now points at freed memory

The rule in one sentence: a view must not outlive the string it looks at, and that string must not be modified while the view is in use. In practice: take views as parameters and use them within the call; return a view only into something the caller gave you (the argument, never a local); never store a view in a struct or container unless the owner is guaranteed to live longer and stay put. words() above is fine because its caller still holds text; it becomes a bug the moment someone writes auto ws = words(read_line()); — the temporary line is gone by the next statement.

One more: data() is not guaranteed to end in '\0'. A view of the middle of a string stops where size() says but the bytes continue, so sv.data() handed to a C function that expects a terminator reads past the window. Build a std::string first.

Converting back

std::string_view sv = "kairo";
std::string s1(sv);              // explicit construction: copies the characters
std::string s2 = sv;             // error: the conversion from a view is explicit
std::string s3;
s3 = sv;                         // fine: assignment from a view is allowed
s3 += sv;                        // fine: append accepts a view
std::string s4 = std::string(sv) + "!";   // build the string, then concatenate

The asymmetry is deliberate: std::string converts to a view silently because that is free; a view becomes a std::string only when you write it, because that allocates. To keep a piece of text — a map key, a struct member, anything that outlives the input — convert and store the std::string.

constexpr views

constexpr std::string_view kVersion = "1.4.2";
constexpr std::string_view kMajor = kVersion.substr(0, kVersion.find('.'));    // "1", at compile time
static_assert(kMajor == "1");
constexpr std::string_view kDays[] = {"mon", "tue", "wed", "thu", "fri", "sat", "sun"};

A view onto a literal is a constexpr value: the characters live in the program image for its whole run, so the lifetime rule holds automatically, and find, substr and == are constexpr. A std::string cannot be a constexpr variable in C++20 (its allocation must not escape compile-time evaluation), so a compile-time table of names is a table of views. Module 16, Modern C++, has more.

When not to use it

  • The function stores the text (a member, a container element, a key): take std::string by value and move it in (Module 9, Copies, moves and the rule of five).
  • You need a null-terminated const char* for a C API: std::string and c_str().
  • The text will be modified: views are read-only windows.
  • The source is a temporary or will change while the view is alive: copy instead.

Pitfalls

  • std::string_view v = s + "x"; or = std::string(...) dangles before the next statement.
  • Returning a view of a local, or of a std::string parameter taken by value, dangles.
  • A view into a std::vector<std::string> element dies on push_back; into a std::string on any resize.
  • sv.data() is not null-terminated; never hand it to printf, fopen or strlen.
  • remove_prefix(n) with n > size() is undefined behaviour; compute n from a find that succeeded.

Key takeaways

  • std::string_view is a pointer and a length: non-owning, sixteen bytes, trivially copyable, read-only.
  • substr, remove_prefix and remove_suffix are O(1) and never allocate; the find family and comparisons work as on std::string.
  • Take it by value as the parameter type for any function that only reads text; return std::string when producing text.
  • A view must not outlive its string or survive a modification of it; never view a temporary, never return a view of a local.
  • Converting back is explicit — std::string(sv) — because it copies; data() is not null-terminated; views of literals are constexpr.

Common questions

Should I use string_view or const std::string& for parameters?

Take std::string_view by value when a function only reads text: a literal, a std::string or another view all pass without a copy, whereas const std::string& builds a temporary string — allocating past 15 characters — when called with a literal. Take std::string when the function stores the text or needs a null-terminated c_str().

When does a string_view dangle?

When the characters it points at are destroyed or moved. std::string_view v = s + "x"; dangles at once, because + returns a temporary; returning a view of a local string dangles; and a view of a std::vector<std::string> element dies when push_back reallocates. A view must not outlive its string, and that string must not change while it is in use.

How do you convert a string_view to a std::string?

Construct one explicitly: std::string s(sv); or std::string(sv). std::string s = sv; does not compile, because the conversion copies and allocates and the library makes you ask for it; assigning s = sv; and appending s += sv; to an existing string are allowed.

Is string_view null-terminated?

Not necessarily. A view of part of a string stops where size() says, but the bytes after it continue, so sv.data() passed to printf, fopen or strlen reads past the window. Build a std::string and use its c_str() when a C API needs a terminator.

Why is string_view substr faster than string substr?

std::string::substr allocates a new string and copies the characters, while std::string_view::substr only adjusts a pointer and a length, in constant time. A tokeniser built from views allocates nothing until you convert a piece you want to keep into a std::string.

Mark as readSign in to keep your place.

C++ String View Explained: When to Use It and Lifetime Rules

A C++17 string view is a non-owning pointer and length with an O(1) substr, the best read-only text parameter. When it dangles, and converting back to a string.

  • Course: C++ study plan
  • Module: Strings
  • Kind: Lesson
  • Reading time: 13 min
  • Runtime: Clang 18 · C++20

What is std::string_view in C++?

std::string_view (C++17) is a non-owning, read-only view of characters that someone else owns: just a pointer and a length, sixteen bytes on x86-64 and cheap to copy. It supports the read-only string interface, including find and an O(1) substr, and accepts literals and std::string without copying. It must never outlive, or survive a modification of, the text it points at.

Lesson

Every std::string you pass, slice or compare costs a copy somewhere: a function taking const std::string& allocates when called with a literal, substr copies what it returns, and a parser that cuts a line into fields copies the whole line again in pieces. std::string_view (C++17) answers all three: a pointer and a length that refer to characters someone else owns. It is sixteen bytes on this platform, trivially copyable, its substr costs nothing — and it brings the one lifetime rule this lesson exists to make you respect.

What it is

#include <string_view>

std::string owner = "codekairo";
std::string_view whole = owner;              // implicit from std::string: points at owner's buffer
std::string_view lit = "literal";            // from a literal: strlen once, no copy
std::string_view part(owner.data() + 4, 5);  // pointer + length: "kairo"
std::cout << whole.size() << ' ' << part << ' ' << lit.substr(0, 3) << '\n';   // 9 kairo lit

A view holds data() and size() and nothing else — no buffer, no ownership, no null terminator of its own; copying one copies two words. It supports the whole read-only string interface: size, empty, [], at, front, back, iteration, the find family, substr, starts_with/ends_with, compare and all six comparison operators, which compare content just as std::string does. Anything that would change the characters — +=, push_back, insert, replace — does not exist, because the view does not own them. Two operations exist only on views: remove_prefix(n) and remove_suffix(n) shrink the window from either end in O(1), and are how a view is trimmed.

std::string_view v = "   padded   ";
v.remove_prefix(v.find_first_not_of(' '));                  // "padded   "
v.remove_suffix(v.size() - v.find_last_not_of(' ') - 1);    // "padded"

substr in O(1)

std::string line = "2026-09-17T10:15:00";
std::string_view sv = line;
std::string_view date = sv.substr(0, 10);     // "2026-09-17": a narrower window, no allocation
std::string_view time = sv.substr(11);        // "10:15:00"

std::string::substr allocates and copies; std::string_view::substr adjusts a pointer and a length in constant time. A tokeniser written with views allocates nothing until you decide to keep something:

#include <vector>

std::vector<std::string_view> words(std::string_view text) {
    std::vector<std::string_view> out;
    std::size_t i = 0;
    while (i < text.size()) {
        while (i < text.size() && text[i] == ' ') ++i;         // skip separators
        const std::size_t start = i;
        while (i < text.size() && text[i] != ' ') ++i;         // run to the end of the word
        if (start < i) out.push_back(text.substr(start, i - start));
    }
    return out;
}

Every element of out points into text's characters. That is the whole point, and the whole danger.

The right parameter type for read-only text

ParameterCalled with a literalCalled with a std::stringKnows its lengthNull-terminated
const char*fineneeds .c_str()no — strlenyes
const std::string&constructs a temporary std::string (allocates past 15 chars)fine, no copyyesyes
std::string_viewfine, no copyfine, no copyyesnot guaranteed

Take std::string_view by value — two words; a reference would be an indirection for nothing — whenever the function only reads text: counting, searching, parsing, printing. The caller passes a literal, a std::string, another view or a (pointer, length) pair and nothing is copied. Return a std::string, not a view, when the function produces text.

int count_char(std::string_view text, char c) {
    int n = 0;
    for (char ch : text) if (ch == c) ++n;
    return n;
}

count_char("banana", 'a');          // 3 - no std::string was ever built
count_char(owner, 'k');             // 1 - no copy of owner

The lifetime rule

A view is valid exactly as long as the characters it points at. Nothing in the language checks this, and the failures are undefined behaviour that often appears to work:

std::string_view bad1 = std::string("temporary");     // the string dies at the ';' - dangling immediately
std::string_view bad2 = owner + "!";                  // same: + returns a temporary

std::string_view first_word() {
    std::string line;
    std::getline(std::cin, line);
    return std::string_view(line).substr(0, line.find(' '));   // line is destroyed on return
}

std::vector<std::string> names = {"ada"};
std::string_view n0 = names[0];
names.push_back("grace");                             // may reallocate: n0 now points at freed memory

The rule in one sentence: a view must not outlive the string it looks at, and that string must not be modified while the view is in use. In practice: take views as parameters and use them within the call; return a view only into something the caller gave you (the argument, never a local); never store a view in a struct or container unless the owner is guaranteed to live longer and stay put. words() above is fine because its caller still holds text; it becomes a bug the moment someone writes auto ws = words(read_line()); — the temporary line is gone by the next statement.

One more: data() is not guaranteed to end in '\0'. A view of the middle of a string stops where size() says but the bytes continue, so sv.data() handed to a C function that expects a terminator reads past the window. Build a std::string first.

Converting back

std::string_view sv = "kairo";
std::string s1(sv);              // explicit construction: copies the characters
std::string s2 = sv;             // error: the conversion from a view is explicit
std::string s3;
s3 = sv;                         // fine: assignment from a view is allowed
s3 += sv;                        // fine: append accepts a view
std::string s4 = std::string(sv) + "!";   // build the string, then concatenate

The asymmetry is deliberate: std::string converts to a view silently because that is free; a view becomes a std::string only when you write it, because that allocates. To keep a piece of text — a map key, a struct member, anything that outlives the input — convert and store the std::string.

constexpr views

constexpr std::string_view kVersion = "1.4.2";
constexpr std::string_view kMajor = kVersion.substr(0, kVersion.find('.'));    // "1", at compile time
static_assert(kMajor == "1");
constexpr std::string_view kDays[] = {"mon", "tue", "wed", "thu", "fri", "sat", "sun"};

A view onto a literal is a constexpr value: the characters live in the program image for its whole run, so the lifetime rule holds automatically, and find, substr and == are constexpr. A std::string cannot be a constexpr variable in C++20 (its allocation must not escape compile-time evaluation), so a compile-time table of names is a table of views. Module 16, Modern C++, has more.

When not to use it

  • The function stores the text (a member, a container element, a key): take std::string by value and move it in (Module 9, Copies, moves and the rule of five).
  • You need a null-terminated const char* for a C API: std::string and c_str().
  • The text will be modified: views are read-only windows.
  • The source is a temporary or will change while the view is alive: copy instead.

Pitfalls

  • std::string_view v = s + "x"; or = std::string(...) dangles before the next statement.
  • Returning a view of a local, or of a std::string parameter taken by value, dangles.
  • A view into a std::vector<std::string> element dies on push_back; into a std::string on any resize.
  • sv.data() is not null-terminated; never hand it to printf, fopen or strlen.
  • remove_prefix(n) with n > size() is undefined behaviour; compute n from a find that succeeded.

Key takeaways

  • std::string_view is a pointer and a length: non-owning, sixteen bytes, trivially copyable, read-only.
  • substr, remove_prefix and remove_suffix are O(1) and never allocate; the find family and comparisons work as on std::string.
  • Take it by value as the parameter type for any function that only reads text; return std::string when producing text.
  • A view must not outlive its string or survive a modification of it; never view a temporary, never return a view of a local.
  • Converting back is explicit — std::string(sv) — because it copies; data() is not null-terminated; views of literals are constexpr.

Common questions

Should I use string_view or const std::string& for parameters?

Take std::string_view by value when a function only reads text: a literal, a std::string or another view all pass without a copy, whereas const std::string& builds a temporary string — allocating past 15 characters — when called with a literal. Take std::string when the function stores the text or needs a null-terminated c_str().

When does a string_view dangle?

When the characters it points at are destroyed or moved. std::string_view v = s + "x"; dangles at once, because + returns a temporary; returning a view of a local string dangles; and a view of a std::vector<std::string> element dies when push_back reallocates. A view must not outlive its string, and that string must not change while it is in use.

How do you convert a string_view to a std::string?

Construct one explicitly: std::string s(sv); or std::string(sv). std::string s = sv; does not compile, because the conversion copies and allocates and the library makes you ask for it; assigning s = sv; and appending s += sv; to an existing string are allowed.

Is string_view null-terminated?

Not necessarily. A view of part of a string stops where size() says, but the bytes after it continue, so sv.data() passed to printf, fopen or strlen reads past the window. Build a std::string and use its c_str() when a C API needs a terminator.

Why is string_view substr faster than string substr?

std::string::substr allocates a new string and copies the characters, while std::string_view::substr only adjusts a pointer and a length, in constant time. A tokeniser built from views allocates nothing until you convert a piece you want to keep into a std::string.

Exercises

Words as views

Read one line. Write std::vector<std::string_view> words(std::string_view text) that splits the text on runs of spaces without copying a single character — every element is a view into the line, which stays alive in main. Then print words=<n>, longest=<word> (the first of the longest words; longest=(none) if there are no words), and one line <word> <length> per word in order.

Input: one line (possibly empty, possibly with leading, trailing or repeated spaces). Output: two lines, then one line per word.

Example: input the quick brown fox → output

words=4
longest=quick
the 3
quick 5
brown 5
fox 3

Prefix filter

The first line is a non-empty prefix. Every following line, until end of input, is an entry. Write std::string_view trim(std::string_view) that removes leading and trailing spaces and tabs with remove_prefix and remove_suffix, and for every entry: trim it, and if the trimmed entry starts_with the prefix print match: followed by the rest of the entry after the prefix. Finally print matched=<k> of <total>, where total counts every entry line, blank ones included.

Input: the prefix, then any number of entry lines. Output: one line per matching entry, then the summary.

Example: input

err
error: disk full
  errno 5
warning: low
err

→ output

match: or: disk full
match: no 5
match:
matched=3 of 4

In this module: Strings

← Formatting output · Checkpoint — Strings →