C++ Part I · Module 9 · Arrays, Strings, and Aggregate Data
std::string_view: Passing Text Without Copying
Chapter 9.5 confirmed Chapter 6.5's parameter rule with a real measurement and then pointed at the gap in it. Taking a const std::string reference says exactly the right thing — I will read this text and not own it — and it still insists that the caller have a std::string to hand. A caller holding a const char pointer from a C interface, or a string literal, or a slice of a larger string, has to build one first, and building one is the allocation that chapter had just finished measuring. So the parameter form that was chosen to avoid a copy can quietly cause one at the call site. This chapter is the type that closes the gap, and it is almost nothing: sixteen bytes containing a pointer and a length, with no storage of its own and no responsibility for what it points at. That makes it free to pass, free to slice, and constructible from every form of text in the preceding two chapters. It also makes it the first type in this curriculum that is dangerous by default rather than by accident, because a thing that does not own its characters can outlive them — which is Chapter 8.8's whole subject, arriving one more time in the one place where the compiler actually warns you about it.
Intermediate16 min readC++Part IFundamentalsStrings
Part I · Module 9 · Chapter 9.6 · std::string_view: Passing Text Without Copying
1. The Gap in Chapter 6.5's Rule
Chapter 6.5 §6's rule was: fundamental types by value, larger read-only things by const&. Chapter 9.5 §6 measured the cost it was avoiding.
But look at what const std::string& requires of a caller:
void log(const std::string& message);
log(text); // fine -- caller has a std::string
log("idle"); // constructs a temporary std::string
log(name_from_c_api); // same, from a const char*
log(whole.substr(0, 8)); // same, plus a copy of the substringThree of those four calls construct a temporary std::string — and whether that allocates depends on how long the text is. Measured, by counting calls to operator new, with a forty-character string:
test text length = 40 inline capacity of an empty string = 22
const std::string& parameter:
from std::string : 0 allocation(s)
from a literal : 1 allocation(s)
from a const char* : 1 allocation(s)
from a char array : 1 allocation(s)
from substr(0,30) : 1 allocation(s)Four of the five allocate. And the qualification matters, because my first attempt at this measurement reported zero for every case: the test strings were short, and Chapter 9.5 §4's small-string optimization meant the temporaries fitted inside their own objects. Past this library's twenty-two-character inline capacity, the allocations appear.
So the honest statement of the gap is narrower than "always allocates": for text longer than the inline capacity, a const std::string& parameter costs an allocation at every call site that does not already hold a std::string. For short text on this implementation it costs nothing — and you do not generally know which case a caller is in.
The reason is that const std::string& asks for a std::string, and a std::string owns its characters — Chapter 9.5 §8. Ownership is exactly what a read-only parameter does not need.
#include <string_view>
void log(std::string_view message); // asks for less, accepts moreA
std::string_viewis a pointer and a length. It does not own the characters, allocate, or copy anything.
2. Measured: No Copy, and Half the Size
#include <iostream>
#include <string>
#include <string_view>
static void by_value(std::string s) { std::cout << " std::string param data at " << static_cast<const void*>(s.data()) << '\n'; }
static void by_view(std::string_view v) { std::cout << " string_view param data at " << static_cast<const void*>(v.data()) << '\n'; }
int main()
{
std::string text = "a reasonably long piece of text that will not fit inline";
std::cout << "caller's data at " << static_cast<const void*>(text.data()) << '\n';
by_value(text);
by_view(text);
std::cout << '\n';
std::cout << "sizeof(std::string) = " << sizeof(std::string) << '\n';
std::cout << "sizeof(std::string_view) = " << sizeof(std::string_view) << '\n';
std::cout << '\n';
std::string_view v = text;
std::cout << "view size = " << v.size() << " first 8: \"" << v.substr(0, 8) << "\"\n";
std::cout << "substr makes a new VIEW, no copy: data at "
<< static_cast<const void*>(v.substr(10, 5).data()) << '\n';
return 0;
}Measured output, zero warnings:
caller's data at 0x102ae99a0
std::string param data at 0x102ae99e0
string_view param data at 0x102ae99a0
sizeof(std::string) = 24
sizeof(std::string_view) = 16
view size = 56 first 8: "a reason"
substr makes a new VIEW, no copy: data at 0x102ae99aaFour readings, and the first is the whole chapter.
The view's characters are at the caller's exact address. 0x102ae99a0 on both the first and third lines. Nothing was copied.
The std::string parameter's characters are somewhere else — 0x102ae99e0. Fifty-six characters were duplicated to produce it.
That pair is Chapter 6.2 §3 and Chapter 6.4 §2's contrast — different address means a copy, same address means none — applied to text rather than to an int. The mechanism has not changed since Module 6; only the type has.
sizeof is 16 against std::string's 24. Sixteen bytes is a pointer and a length: 8 + 8, on this platform. There is no room for anything else and nothing else is needed.
And substr costs nothing. v.substr(10, 5) reported its data at 0x102ae99aa, which is the base address plus 10. It returned a new view onto the same characters — a different pointer and a different length, no allocation, no copying. Chapter 9.5's std::string::substr allocates and copies; this one is arithmetic.
3. It Accepts Everything
A std::string_view can be built from every form of text in this module:
void log(std::string_view message);
std::string s = "idle";
const char* c = get_name(); // from a C interface
const char a[] = "ready";
log(s); // from a std::string -- no allocation
log("idle"); // from a literal -- no allocation
log(c); // from a const char* -- no allocation
log(a); // from a char array -- no allocation
log(s.substr(0,2)); // this one DOES allocate -- it is string::substrMeasured against the same five calls, with the same forty-character text:
std::string_view parameter:
from std::string : 0 allocation(s)
from a literal : 0 allocation(s)
from a const char* : 0 allocation(s)
from a char array : 0 allocation(s)
from view substr : 0 allocation(s)Zero in every case, where §1's table showed four allocations out of five. The last row uses std::string_view(s).substr(0, 30); writing s.substr(0, 2) instead would allocate, because that is std::string::substr returning a std::string.
That is §1's gap closed. One parameter type serves every caller, and none of them has to construct anything.
From a const char* it calls strlen. Worth knowing: constructing a view from a null-terminated pointer has to find the length, so it is O(n) once — Chapter 9.4 §3's scan, done one time at the boundary instead of on every use. Constructing from a std::string is O(1), because the length was already stored.
4. What It Is Not
Three things, because each is a plausible mistake.
It is not null-terminated. A view is a pointer and a length; there is no guarantee of a zero byte after the last character, and v.substr(0, 8) in §2 is a clear case — it views the first 8 characters of a longer string, and the ninth character is 'a', not '\0'.
So you cannot hand v.data() to a C interface. This is the one real inconvenience of the type, and the fix is explicit:
some_c_function(std::string(v).c_str()); // construct a string -- allocates, and is correctChapter 9.4 §8 said every OS interface takes a const char*. A view is the wrong type at that boundary, and std::string is the right one.
It is not mutable. A view gives read-only access to the characters. There is no v[0] = 'x'. If you need to modify text, you need something that owns it — Chapter 9.5.
It is not an owner, which is §5 onward.
5. The One Rule
The viewed characters must outlive the view.
A view holds a pointer. Chapter 8.1 §6's callout named the hard thing about pointers: the value refers to something whose existence it does not control. A std::string_view is that property, in a type whose entire purpose is to be passed around.
The failure looks like this:
#include <iostream>
#include <string>
#include <string_view>
static std::string make_name() { return std::string("a long temporary string value"); }
int main()
{
std::string_view v = make_name(); // the temporary dies at the end of THIS statement
std::cout << v << '\n'; // the view now points at destroyed characters
return 0;
}make_name() returns a std::string by value — a temporary. The view is initialized from it, taking a pointer to its characters. The temporary is destroyed at the end of that statement, releasing those characters. The view survives into the next line, pointing at storage that has been freed.
This is Chapter 8.8 §6's case exactly: a reference-like thing bound to something inside a temporary, where Chapter 6.5 §5's lifetime extension does not apply because the view is not a reference and the binding is not direct.
6. The Good News: Here the Compiler Warns
$ c++ -std=c++17 -Wall -Wextra -Wpedantic viewdangle.cpp -o viewdangle
viewdangle.cpp:9:26: warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]At the standing flags. No extra flag, no sanitizer required.
And under the sanitizer as well:
==56590==ERROR: AddressSanitizer: heap-use-after-free on address 0x603000001d50
SUMMARY: AddressSanitizer: heap-use-after-free (libsystem_c.dylib) in __sfvwriteThis is worth stating against Chapter 8.8 §8's table, because a reader arriving from that chapter will reasonably assume no help is available:
| Lifetime bug | Compiler at the standing flags |
|---|---|
| returned reference to a local (8.8 §3) | warns |
| use after scope (8.8 §4) | silent |
| pointer into a reallocated container (8.8 §5) | silent |
| reference into a temporary argument (8.8 §6) | silent, even with -Wdangling |
| a view onto a temporary (here) | warns -Wdangling-gsl |
Two of six, and this is one of them. The reason the compiler can see it is that std::string_view is annotated in the standard library as a non-owning view, which is what gsl in the warning's name refers to — the compiler has been told that this type borrows, so it can check the one rule.
That does not make the type safe. The warning catches the direct case in §5; it does not catch a view stored in a structure, returned from a function, or kept across a modification of the string it views. Chapter 8.8's model is still the thing you reason with.
7. Where the Rule Bites in Practice
Four patterns, in increasing order of how easy they are to get wrong.
A parameter — always safe. void log(std::string_view) is the canonical use. The caller's text necessarily outlives the call, so the rule is satisfied by construction. If you only take one thing from this chapter, it is that this is the right parameter type for read-only text.
A local initialized from a named string — safe, with care. std::string_view v = s; is fine while s is unmodified and alive. Chapter 9.5 §5 measured a string's characters moving on append, so the rule has two halves here: do not outlive s, and do not survive a modification of s.
A return value — usually wrong. Returning a std::string_view from a function means promising that the characters outlive the call, which is only true if they belong to something the caller already owns. Returning a view onto a local std::string is Chapter 8.8 §3's bug with a new spelling.
A member of a stored object — needs a documented contract. A structure holding a view is holding a promise about something else's lifetime. It can be correct and it has to be deliberate, and Chapter 9.11 is where this kind of decision is the subject.
8. What to Actually Do
Take std::string_view for read-only text parameters. It is the right default and it accepts every caller.
Keep const std::string& when the function needs a std::string specifically — to copy it, store it, or call a std::string member the view lacks.
Take std::string& when you must modify the text — a view cannot.
Use the view's substr for slicing; it is arithmetic, where std::string::substr allocates.
Do not pass v.data() to a C interface. A view is not null-terminated; construct a std::string at that boundary.
Never return a view onto something local.
Treat a view as invalid after the string it views is modified — Chapter 9.5 §5.
If you store a view, document whose characters it refers to. The lifetime is now your contract.
9. Mistakes Worth Making Once
Assuming a view is null-terminated. It is a pointer and a length; substr views the middle of other text.
Passing v.data() to fopen or similar. No terminator — construct a std::string.
Initializing a view from a temporary. Measured: -Wdangling-gsl at the standing flags, and heap-use-after-free under ASan.
Returning a view onto a local. Chapter 8.8 §3's bug, respelled.
Keeping a view across an append. Chapter 9.5 §5 measured the characters moving.
Using std::string::substr when you wanted a slice. It allocates; the view's does not.
Trying to modify through a view. It is read-only by design.
Assuming the -Wdangling-gsl warning covers every case. It catches the direct initialization; it does not follow a view into a structure or out of a function.
10. Interview Relevance
"What is std::string_view and why does it exist?" A non-owning pointer-and-length view over character data. The reason is the gap in §1: const std::string& still forces callers holding a literal or a const char* to allocate. The measurement to have ready is the address pair — the view parameter shared the caller's exact character address.
"When would you not use it?" When you need to own, store or modify the text, and at a C boundary — because it is not null-terminated. Knowing the terminator limitation is the detail that separates use from recital.
"What is the risk?" It does not own the characters, so they must outlive the view. Returning one onto a local, or keeping one across a modification, are the two common failures.
"Is std::string_view cheap to copy?" Yes — 16 bytes, a pointer and a length, which is why it is passed by value rather than by const&.
11. What You Should Be Able to Say Now
const std::string&has a gap: three of §1's four calls allocate, because the parameter demands astd::stringand astd::stringowns.- A
std::string_viewis a pointer and a length — no ownership, no allocation, no copy. - Measured: the view parameter's data address was identical to the caller's (
0x102ae99a0); thestd::stringparameter's was different. - That is Chapter 6.2 and 6.4's address contrast, applied to text.
- Measured:
sizeofis 16 againststd::string's 24 — a pointer plus a length. - Measured:
v.substr(10, 5)reported data at base + 10 — a new view, no allocation.std::string::substrallocates. - It accepts a
std::string, a literal, aconst char*and achararray, all without allocating. - From a
const char*the construction is O(n) — onestrlenat the boundary; from astd::stringit is O(1). - It is not null-terminated, so it is the wrong type at a C interface; construct a
std::stringthere. - It is not mutable and not an owner.
- The one rule: the viewed characters must outlive the view.
- Measured, and this is the good news: initializing a view from a temporary warns at the standing flags —
-Wdangling-gsl— where the compiler was silent on three of Chapter 8.8 §8's four cases. - It warns because the library annotates the type as non-owning; it does not follow a view into a structure or out of a function.
- The four text forms differ in exactly two independent properties — knows its length and owns the characters — and the view is the one that deliberately has the first without the second.
12. Exercises
Exercise 1 — which calls allocate? For void log(const std::string& m) and void log(std::string_view m), say which of these allocate under each signature: (a) a std::string variable (b) "idle" (c) a const char* (d) s.substr(0, 2).
Exercise 2 — read the addresses. §2 printed three addresses. Say which two are equal, what that proves, and which earlier chapter used the same technique for an int.
Exercise 3 — the terminator. Explain why v.data() cannot be passed to std::fopen, using v.substr(0, 8) from §2 as the example, and give the correct call.
Exercise 4 — safe or not? For each, say whether the rule is satisfied: (a) a std::string_view parameter (b) std::string_view v = s; then read v (c) std::string_view v = s; then s += "x" then read v (d) returning a view onto a local std::string (e) returning a view onto a static string.
Exercise 5 — the warning's reach. std::string_view v = make_name(); warns. Give two variations that have the same defect and do not warn, and say why the compiler loses track.
Exercise 6 — fill in the table. Reproduce §7's four-row table from memory — the two properties and which of the four text forms has which.
Exercise 7 — choose the parameter. Give the type for each and one clause of reasoning: (a) print some text (b) store the text in a structure that outlives the call (c) uppercase the caller's text in place (d) parse a field out of a larger string without copying (e) pass text to std::fopen.
13. Summary
const std::string&still demands an owner — three of four call forms allocate.- A
std::string_viewis a pointer and a length: no ownership, no allocation. - Measured: the view parameter shared the caller's exact character address; the
std::stringparameter did not. - Measured: 16 bytes against 24;
substrreturned a view at base + 10 with no allocation. - It accepts strings, literals,
const char*and arrays — O(n) once from a pointer, O(1) from a string. - Not null-terminated, not mutable, not an owner — so construct a
std::stringat a C boundary. - The one rule: the viewed characters must outlive the view.
- Measured: a view onto a temporary warns at the standing flags (
-Wdangling-gsl) — two of Chapter 8.8's six cases now get compile-time help. - The warning does not follow a view into a structure or out of a function.
- Own it if you store it, view it if you read it, and write down whose characters a stored view refers to.
Six chapters have now been about sequences of one thing — ints back to back, characters back to back, a length attached to them or not. Every type so far has answered the question how many of these do I have. The module's remaining five chapters answer a different one. A register is not four integers; it is an address and a width and a field and an access mode, which are four things of different kinds that only mean something together. Representing that needs a way to group values that are not alike, and the next chapter is that — including the member-access notation Chapter 8.2 §7 deferred because there was nothing yet with members to reach into.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
