C++ std::string_view: Fast Read-Only Text APIs Without Accidental Copies
Learn when a read-only text API should take std::string_view, then trace one request parser and spot the lifetime errors that make views dangle.
KnowledgeGate Team
Exam prep & CS education

You replaced pass-by-value with references, but how should one read-only function accept a std::string, a literal, or a cheap slice? std::string_view is a non-owning view, a request-line parser can split it without copying characters, and lifetime rules prevent dangling views. For broader language and interview practice, use the Coding & Skill Development Courses.
Related reading: arrays and strings in C and C++ arrays and spans.
std::string_view is a non-owning window, not a smaller string
Since C++17, std::string_view has represented a read-only view over contiguous characters. Think of it as a data pointer plus a length. Copying that descriptor copies no characters, and destroying it frees no text. The standard promises no particular size or layout.
A std::string owns its character storage. A view can observe all or part of it, but cannot keep the owner alive. Read-only access through the view does not make that owner immutable.
std::string owner = "alpha-beta";
std::string_view whole = owner;
std::string_view left = whole.substr(0, 5);Here, whole.size() == 10, left == "alpha", and substr copies zero characters. For the wider language sequence, use the C++ tutorial learning path.
Choose the function parameter by what the function must do
Suppose the lvalue std::string line contains the 30 characters GET /api/course?id=42 HTTP/1.1.
Signature | What happens for | What happens for |
|---|---|---|
| Copies 30 characters into the parameter | Creates an owning string parameter |
| Copies no characters for this call | Requires a temporary |
| Copies no characters, only a view is passed | Views the five literal characters without first creating an owning string |
Do not infer an allocation count. Small-string optimisation and allocation strategies are implementation details.
Accept std::string_view by value when the function only reads during the call. Use std::string when it must own, retain, or modify the text. Use const std::string& when the API specifically requires an existing owning string or its interface.
Worked example: split one request line without copying text
The complete input is:
std::string request = "GET /api/course?id=42 HTTP/1.1";It has 30 characters at indices 0 through 29. Spaces occur at 3 and 21; the question mark is at full-line index 15, or target index 11.
#include <stdexcept>
#include <string>
#include <string_view>
struct RequestParts {
std::string_view method, target, path, query, protocol;
};
RequestParts split_request(std::string_view line) {
const auto first_space = line.find(' ');
if (first_space == std::string_view::npos) {
throw std::invalid_argument("missing first space");
}
const auto second_space = line.find(' ', first_space + 1);
if (second_space == std::string_view::npos) {
throw std::invalid_argument("missing second space");
}
const auto method = line.substr(0, first_space);
const auto target = line.substr(
first_space + 1, second_space - first_space - 1);
const auto question = target.find('?');
std::string_view path = target;
std::string_view query;
if (question != std::string_view::npos) {
path = target.substr(0, question);
query = target.substr(question + 1);
}
const auto protocol = line.substr(second_space + 1);
return {method, target, path, query, protocol};
}For this input, method = line.substr(0, 3) is "GET", length 3, offset 0. target = line.substr(4, 17) is "/api/course?id=42", length 17, offset 4. path has length 11 at offset 4, and query has length 5 at offset 16. protocol = line.substr(22) is "HTTP/1.1", length 8, offset 22.
Those are five descriptors into the same 30-character buffer, so the successful slices copy zero characters. Returning them is safe only while caller-owned request remains alive and no operation invalidates its storage.

Cheap substrings still need correct bounds and output handling
substr(pos, count) returns another view and clips count to the remaining length, but pos > size() throws std::out_of_range. Reject missing spaces before arithmetic. Never add 1 blindly to npos.
Descriptor-only cursor movement is cheap too:
std::string_view cursor = target;
cursor.remove_prefix(5);Now cursor is "course?id=42", length 12. target and its owner are unchanged. No characters were copied or erased.
A view is not necessarily null-terminated. target.data() points at index 4, but index 21 is a space, not \0. A C API reading %s could continue into HTTP/1.1. Prefer a length-aware API, stream the view, use printf("%.*s", static_cast<int>(target.size()), target.data()) when the size fits int, or create a std::string at that boundary.
Lifetime is the contract: three ways a view starts dangling
These two views outlive their owners:
std::string_view bad = std::string("id=42"); // owner dies at this semicolon
std::string_view make_label() {
std::string local = "id=42";
return local; // local dies when the function exits
}Reading either dangling view is undefined behaviour, even if one run appears to leave the old bytes unchanged.
Reallocation can invalidate a view just as decisively:
std::string owner = "id=42";
std::string_view v = owner;
const auto old_capacity = owner.capacity();
owner.append(old_capacity + 1, '!');
// The new size exceeds old_capacity. Do not use v now.The append forces larger storage because the new size exceeds the recorded capacity. Mutation without reallocation can change what a valid view observes.
Keep the caller's owner alive until every returned view is finished. Return std::string for local results, and copy a view before retaining it. constexpr std::string_view method = "GET" is safe because literal characters have static storage duration. The Pointers in C memory-diagram guide reinforces the backing-storage question; these C++ rules determine validity.

Avoid accidental copies without avoiding necessary ownership
Use case | Suitable representation |
|---|---|
Search, parse, or compare entirely during a call |
|
Store a cache key after return |
|
Mutate text in place |
|
Call a legacy C API that requires null termination | A null-terminated |
A std::vector<std::string_view> is safe only when every owner outlives it and keeps stable storage. To retain five fields after releasing the input, store five strings, or keep one owning buffer with clearly tied views.
Use testable claims. string_view avoids character copies at specified boundaries, not always. It carries a length, not a null-termination guarantee. A const string_view keeps no source constant or alive.
How interviews and assessments test std::string_view
Typical prompts ask for the five (offset, length) pairs, both owners' destruction points, why %s can print beyond id=42, or a cache-key type. Answers: (0,3), (4,17), (4,11), (16,5), (22,8); the assignment semicolon and function exit; a space at index 21; and owned cache storage.
Why does split_request(std::string("GET /api/course?id=42 HTTP/1.1")) return dangling parts? Its temporary owner dies at the end of the call's full expression.
Use a 20-second routine: find the owner, mark offset and length, locate destruction or invalidation, check retention, then check whether the next API is length-aware.
The short version and the next C++ practice step
Remember four rules: a view owns no characters; pass it by value for read-only work during the call; every substring shares an owner's lifetime; use std::string for ownership, mutation, or null termination. The result is five zero-character-copy views over one 30-character owner.
Use the C++ Programming course as the structured next step for broader concepts and practice. Coding for Placements is the alternative when you are applying C++ alongside other languages in placement preparation.
Now extend split_request for a missing query delimiter. Test "GET /api/course HTTP/1.1", which should produce an empty query and path "/api/course", then test "GET /api/course?id=42 HTTP/1.1", which should keep path "/api/course" and query "id=42". In both tests, keep each owner alive until every view has been consumed.
Keep learning

C++ unordered_map and unordered_set: Hashing, Equality and a Custom-Key Frequency Counter
Build a correct mental model for C++ unordered containers, then trace a custom-key counter through a real collision without merging distinct keys.

C++ STL Containers: Choose by Access Pattern, Mutation Cost and Ownership
Choose a C++ STL container from the workload, not from habit. Trace sequence operations, keyed counts, invalidation and object lifetime through exact examples.

C++ STL Algorithms: Sort, Search, Transform and Clean Data in One Pipeline
Trace nine sensor readings through sort, lower_bound, transform, erase-remove and accumulate, with every iterator rule and intermediate value explained.

C++ std::vector Internals: Growth, Capacity, Reallocation and Iterator Invalidation
Learn how std::vector manages contiguous storage, why an append can relocate every element, and which iterators, pointers and references survive each mutation.