string_view and Non-Owning Views

Reviewed & published by Brayan K

A std::string_view is a tiny window — just a pointer and a length — over characters that live somewhere else. It lets you slice strings and accept text parameters with zero copying. The catch is that it doesn't own its data, so it can dangle. By the end you'll use views to make string code faster, and know exactly when they are safe.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A std::string is like photocopying a page of a book so you can keep your own copy. A std::string_view is like sliding a cardboard frame over part of the original page — you can read the words inside the frame, and you can move or shrink the frame instantly, but you never copy anything. The danger is obvious: if someone closes the book (destroys the string) or reshuffles the pages (reallocates the buffer) while your frame is still resting on it, the frame now sits over nothing. The frame only works while the page underneath stays put.

1. Owning vs Viewing

A std::string owns its characters: constructing one allocates memory and copies the data in. A std::string_view owns nothing — it stores only a pointer to the first character and a length, describing a window into characters that already exist. That makes copying a view trivially cheap (so you pass it by value), and it makes a substring of a view free: substr, remove_prefix, and remove_suffix just move the window's edges without touching the data.

#include <iostream>
#include <string>
#include <string_view>
using namespace std;

int main() {
    // A std::string OWNS its characters (it allocates and copies).
    string owner = "Hello, world";

    // A std::string_view is just a POINTER + LENGTH into existing data.
    // No allocation, no copy -- it borrows the characters.
    string_view view = owner;

    cout << "size: " << view.size() << endl;  // size: 12

    // A substring of a VIEW is another view: it only shifts the pointer
    // and shrinks the length. Zero characters are copied.
    string_view hello = view.substr(0, 5);    // "Hello"
    cout << "slice: " << hello << endl;       // slice: Hello

    // remove_prefix / remove_suffix just move the window's edges:
    string_view tail = view;
    tail.remove_prefix(7);                    // drop "Hello, "
    cout << "tail: " << tail << endl;         // tail: world
    return 0;
}

// ✅ Expected output:
//    size: 12
//    slice: Hello
//    tail: world

2. string_view as a Function Parameter

The classic read-only string parameter was const std::string&. But when a caller passes a string literal or a char array, that forces the construction of a temporary std::string — an allocation and a copy just to call the function. A std::string_view parameter binds directly to std::string, literals, and char arrays alike, with no allocation, while still offering size(), substr() and the other read operations. For read-only text, string_view is the modern default.

#include <iostream>
#include <string>
#include <string_view>
using namespace std;

// Accepting string_view means this function binds to ALL of:
//   - a std::string        (no copy)
//   - a string literal     (no temporary std::string built)
//   - a char array / char*
// ...with zero allocations, while still offering size(), substr(), etc.
void printLine(string_view text) {
    cout << "[" << text.size() << "] " << text << endl;
}

int main() {
    string s = "from a std::string";
    printLine(s);                    // [18] from a std::string
    printLine("a string literal");   // [16] a string literal  (no temp!)

    // Compare: a 'const std::string&' parameter would force the literal
    // above to be turned into a temporary std::string -> an allocation.
    return 0;
}

// ✅ Expected output:
//    [18] from a std::string
//    [16] a string literal

Your turn. Complete firstWord so it returns a view of the text before the first space — no copying allowed:

#include <iostream>
#include <string>
#include <string_view>
using namespace std;

// firstWord returns a VIEW of the first word -- no copying.
string_view firstWord(string_view s) {
    auto space = s.find(' ');
    // 🎯 YOUR TURN — return the part of s BEFORE the first space.
    // Use substr from index 0 up to 'space' characters:
    return ___;          // 👉 s.substr(0, space)
}

int main() {
    string sentence = "view borrows characters";
    cout << firstWord(sentence) << endl;  // view
    // ✅ Expected output:  view
    return 0;
}

🧩 Reorder Challenge

These lines should make a string, view it, slice the view, and print the slice. Put them in the right order:

A.     string_view v = owner;
B.     string owner = "Hello, world";
C.     cout << v.substr(0, 5) << endl;
D. int main() {
E.     return 0;
F. }

Correct order: D → B → A → C → E → F

Open main (D), create the owning string (B), make a view of it (A), print a zero-copy slice (C), then return 0; (E) and close the brace (F). The owner must exist before the view.

3. The Dangling Danger

Because a view doesn't own its data, it is only valid while that data lives. Three patterns are undefined behaviour: returning a view of a local string (the local dies on return), storing a view of a temporary (the temporary dies at the end of the statement), and mutating the owner while a view of it exists (appending can reallocate the buffer, invalidating the view's pointer). The rule of thumb: a string_view must never outlive the data it points at, and the owner must not be reallocated underneath it.

#include <iostream>
#include <string>
#include <string_view>
using namespace std;

// ❌ BUG: returns a view of a LOCAL string. When makeView returns, the
// local 'temp' is destroyed -- the returned view dangles immediately.
string_view makeView() {
    string temp = "I will be destroyed";
    return temp;                 // 🚨 dangling: temp dies here
}

int main() {
    // Using a dangling view is UNDEFINED BEHAVIOUR (garbage or a crash):
    // string_view bad = makeView();
    // cout << bad << endl;      // 🚨 do NOT do this

    // ✅ The fix: make the data outlive the view. Keep an owner around,
    // or return an owning std::string instead of a view.
    string owner = "I stay alive";
    string_view ok = owner;      // owner outlives ok -> safe
    cout << ok << endl;          // I stay alive

    // Another trap: mutating the owner can REALLOCATE its buffer,
    // leaving the view pointing at freed memory:
    string buf = "abc";
    string_view v = buf;
    // buf += "def...";          // 🚨 may reallocate -> v now dangles
    cout << v << endl;           // abc  (safe only because buf is untouched)
    return 0;
}

// ✅ Expected output:
//    I stay alive
//    abc

No null terminator: a string_view can point at a slice in the middle of a buffer, so there is no guaranteed trailing \0. Never hand view.data() to a C API that expects a null-terminated string — build a std::string from the view first and use its .c_str().

4. std::span — The Array Analogue

std::span<T> (C++20) takes the exact same idea — a non-owning pointer plus length — and applies it to any contiguous sequence: the elements of a std::vector, a std::array, or a raw C buffer. A function taking span<const int> accepts all of them without copying, just as string_view does for characters. The same caveat applies: a span borrows, so it must not outlive its data, and subspan only adjusts the window.

#include <iostream>
#include <vector>
#include <span>          // C++20
using namespace std;

// std::span<T> is "string_view for arrays": a non-owning pointer+length
// view over any contiguous sequence -- a vector, an array, a C buffer.
int sum(span<const int> values) {
    int total = 0;
    for (int v : values) total += v;
    return total;
}

int main() {
    vector<int> v = {1, 2, 3, 4};
    int raw[] = {10, 20, 30};

    cout << "vector sum: " << sum(v)   << endl;  // vector sum: 10
    cout << "array  sum: " << sum(raw) << endl;  // array  sum: 60

    // Like string_view, a span borrows -- it must not outlive its data,
    // and operations like subspan only adjust the window, never copy.
    span<const int> tail = span<const int>(v).subspan(2); // {3, 4}
    cout << "tail sum:   " << sum(tail) << endl;  // tail sum: 7
    return 0;
}

// ✅ Expected output:
//    vector sum: 10
//    array  sum: 60
//    tail sum:   7

🔎 Deep Dive: own vs borrow, and how to convert

Borrow by default, own when you must outlive the source. Use a string_view parameter for read-only text that you only need during the call. The moment you need to store the characters past the lifetime of the source — in a member, a container, or a returned value — copy them into an owning std::string.

Conversion goes both ways and is explicit in the dangerous direction: a std::string converts to a string_view implicitly (cheap, borrowing), but turning a string_view back into an owning string requires you to write std::string(sv) — a deliberate copy that makes the ownership transfer visible.

void use(string_view sv);          // borrow: fine for the duration of the call
struct Cache { string text; };      // OWN: must copy in
Cache make(string_view sv) {
    return Cache{ string(sv) };     // copy the viewed chars into an owner
}
// sv may dangle after this; the Cache's std::string is safe to keep

Pro Tips

🧠 Quick Recall

Predict the result before revealing the answer.

1. Is this safe?

string_view f() {
    string local = "hi";
    return local;
}

No — local is destroyed when f returns, so the view dangles. Return an owning std::string instead.

2. How many copies does this make?

string_view v = "Hello, world";
string_view w = v.substr(0, 5);

Zero — substr on a view just shifts the pointer and length; no characters are copied.

3. What can go wrong here?

string s = "abc";
string_view v = s;
s += "defghijklmnop";
cout << v;

The append may reallocate s, leaving v pointing at freed memory — undefined behaviour. Don't mutate the owner while a view is alive.

Common Errors (and the fix)

📋 Quick Reference

TaskCode
View a stringstring_view v = s;
Read-only paramvoid f(string_view t)
Zero-copy slicev.substr(0, 5)
Trim the windowv.remove_prefix(2)
Lengthv.size()
Copy to ownerstring s(v);
Array viewspan<const int> a = vec;

Mini-Challenge: Count Words Without Copying

Write countWords(string_view) that counts space-separated words by walking the view with find and remove_prefix — never building a std::string, never copying.

#include <iostream>
#include <string>
#include <string_view>
using namespace std;

// 🎯 MINI-CHALLENGE: count words without copying
// 1. Write  int countWords(string_view text)  that returns how many
//    space-separated words are in text.
// 2. Walk the view with find(' ') and remove_prefix to skip past each
//    word + space -- never build a std::string, never copy.
// 3. In main, call it on a sentence and a string literal.
//
// ✅ Example:  countWords("one two three") == 3

// your code here

🎉 Lesson Complete

Practice quiz

What does a std::string_view actually store?

  • A pointer to char data plus a length
  • A full owning copy of the characters
  • A reference count and a buffer
  • A null-terminated C string only

Answer: A pointer to char data plus a length. A string_view is just a pointer and a length — a lightweight, non-owning window over character data that lives elsewhere.

Why is taking a substring of a string_view cheap?

  • It uses move semantics on the buffer
  • It compresses the data
  • It only adjusts the pointer and length — no characters are copied
  • It caches the result

Answer: It only adjusts the pointer and length — no characters are copied. substr on a view returns another view by shifting the pointer and shrinking the length; no allocation or copy happens.

Which parameter type lets a function accept both std::string and string literals without copying?

  • std::string by value
  • std::string_view
  • char*
  • const std::string&

Answer: std::string_view. A std::string_view parameter binds to std::string, string literals, and char arrays alike, all without an allocation or copy.

What is the main danger of std::string_view?

  • It is always slower than std::string
  • It cannot be passed to functions
  • It copies too much memory
  • It can dangle if the data it views is destroyed before the view

Answer: It can dangle if the data it views is destroyed before the view. A view does not own its data, so if the underlying string is destroyed or reallocated, the view becomes a dangling reference.

Does std::string_view guarantee its data is null-terminated?

  • No — a view may point into the middle of a larger buffer with no terminator
  • Only if created from a literal
  • Only on Windows
  • Yes, always

Answer: No — a view may point into the middle of a larger buffer with no terminator. A view can reference a slice of a buffer, so there is no guaranteed '\0' at the end; never pass view.data() to C APIs expecting a C string.

Which of these is a SAFE use of string_view?

  • Storing a view of a temporary that has already been destroyed
  • Reading a substring of a string that stays alive for the whole call
  • Viewing a string and then resizing that string
  • Returning a view of a local std::string from a function

Answer: Reading a substring of a string that stays alive for the whole call. A view is safe as long as the data it points at outlives the view and is not reallocated; reading a stable string during a call is fine.

What is the array analogue of string_view in C++20?

  • std::list
  • std::vector
  • std::array
  • std::span<T>

Answer: std::span<T>. std::span<T> is a non-owning view (pointer + length) over a contiguous sequence of any element type — string_view for arrays.

What happens to a std::string_view's contents if you call push_back on the std::string it views?

  • The view updates automatically
  • Nothing can go wrong
  • The string may reallocate, leaving the view dangling
  • The view grows too

Answer: The string may reallocate, leaving the view dangling. Appending can force the string to reallocate its buffer; the old pointer the view held is then invalid — a classic dangling bug.

Why prefer string_view over const std::string& for a read-only parameter?

  • It owns the data more safely
  • It avoids forcing callers with a literal or char array to build a temporary std::string
  • It is null-terminated
  • It allows modifying the string

Answer: It avoids forcing callers with a literal or char array to build a temporary std::string. A const std::string& parameter forces a literal or char array to be converted into a temporary std::string; a string_view binds directly with no allocation.

Which operation is NOT available on std::string_view?

  • push_back
  • substr
  • size
  • remove_prefix

Answer: push_back. A view is read-only and non-owning, so it cannot modify or grow the underlying data; push_back belongs to std::string, not string_view.

Continue this course

Frequently asked questions

What exactly is std::string_view?

It is a lightweight, non-owning view over a sequence of characters — internally just a pointer to the first character and a length. It does not allocate, does not copy, and does not own the data; it merely describes a window into characters that live somewhere else (a std::string, a string literal, a char array). Because it is so cheap to copy, you pass it by value.

When should I use std::string_view as a function parameter?

Use it for read-only string parameters. A const std::string& parameter forces callers who pass a string literal or a char array to materialise a temporary std::string, which allocates. A std::string_view binds directly to std::string, literals, and char arrays with no allocation and no copy, while still giving you size(), substr(), and the usual read operations. Switch to const std::string& only when the function genuinely needs an owning string.

What is the biggest danger with string_view?

Dangling. A view does not keep its data alive, so if the underlying buffer is destroyed — or reallocated, for example by appending to the std::string it views — the view points at freed or moved memory and using it is undefined behaviour. Never return a string_view that refers to a local string, never store a view of a temporary, and never mutate the owner while a view of it is in use.

Is a string_view null-terminated?

No. A view can reference a slice in the middle of a larger buffer, so there is no guarantee of a trailing '\0'. That means you must not pass view.data() to C functions that expect a null-terminated C string (like printf %s or fopen). If you need a real C string, build a std::string from the view first and call .c_str() on that.

How does std::span relate to string_view?

std::span<T> (C++20) is the same idea generalised to any element type: a non-owning pointer-plus-length view over a contiguous range, such as the elements of a std::vector or a C array. Where string_view is the read-only window over characters, span is the window over arrays. span can also be mutable, letting you read and write the underlying elements without owning them.

Related lessons