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

Updated 22 Sep 20266 min read

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.

cpp
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 contains_id(line)

What happens for contains_id("id=42")

bool contains_id(std::string text)

Copies 30 characters into the parameter

Creates an owning string parameter

bool contains_id(const std::string& text)

Copies no characters for this call

Requires a temporary std::string conversion if this is the only overload

bool contains_id(std::string_view text)

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:

cpp
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.

cpp
#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.

A 30-cell buffer holding GET /api/course?id=42 HTTP/1.1, with the method, target, path, query and protocol views bracketed above it.

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:

cpp
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:

cpp
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:

cpp
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.

Three lanes compare a safe caller-owned request, a temporary string that dies at the semicolon, and an append that moves the buffer.

Avoid accidental copies without avoiding necessary ownership

Use case

Suitable representation

Search, parse, or compare entirely during a call

std::string_view

Store a cache key after return

std::string

Mutate text in place

std::string& or another owned string

Call a legacy C API that requires null termination

A null-terminated std::string

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.