Skip to content
C++
Library
since C++17
Beginner

std::string_view

Non-owning view of a character sequence. Zero-copy alternative to const std::string&. Accepts literals, std::string, and char arrays without allocation.

std::string_viewsince C++17

A non-owning view into a contiguous sequence of characters, providing efficient read-only access to strings without copying or allocation.

Overview

std::string_view solves a fundamental problem in pre-C++17 code: accepting string parameters efficiently. Before C++17, you had to choose between const std::string& (which forces allocation when passed string literals) and const char* (which requires overloads and lacks bounds information).

std::string_view is a tiny object—just a pointer and a length—that refers to a character sequence you don't own. It accepts string literals, std::string, char*, and substrings with zero allocation and zero copies. It's ideal for function parameters accepting read-only strings.

The critical constraint: never store it. The underlying data must outlive the view, or you have a dangling pointer.

Syntax

Construction

cpp
#include <string_view>

std::string_view sv1;                           // C++17: empty view
std::string_view sv2 = "hello";                 // C++17: from literal
std::string_view sv3(ptr, len);                 // C++17: pointer + length
std::string_view sv4 = some_string;             // C++17: from std::string (implicit)

// C++20: alternate notation
using namespace std::string_view_literals;
auto sv5 = "hello"sv;                           // C++17 as literal operator

Core interface

cpp
// C++17
sv.size();              // length in characters
sv.empty();             // true if size() == 0
sv.data();              // raw pointer (not guaranteed null-terminated)
sv[i];                  // unchecked access
sv.at(i);               // checked access (throws std::out_of_range)
sv.substr(pos, len);    // returns new string_view (no copy)
sv.remove_prefix(n);    // advance start by n
sv.remove_suffix(n);    // shrink end by n
sv.find(x);             // find char, string, or view; returns npos if not found
sv.compare(other);      // lexicographic comparison

// C++20
sv.starts_with(x);      // prefix check (x: char, string_view, or char*)
sv.ends_with(x);        // suffix check
sv.substr(pos);         // substring from pos to end (length deduced)

// C++23
sv.contains(x);         // substring presence check

Examples

Replace overloads with a single parameter

cpp
// C++17 – before string_view
void process(const std::string& s);    // needs std::string
void process(const char* s);           // needs char*
// Callers of process("hello") force a temporary std::string

// C++17+ – with string_view
void process(std::string_view sv) {
    std::cout << sv << '\n';
}

int main() {
    process("hello");                   // string literal – no alloc
    std::string s = "world";
    process(s);                         // std::string – no copy
    process(std::string_view(s).substr(1, 3));  // substring – no copy
}

Substring views without allocation

cpp
std::string_view text = "Hello, World!";

// All of these return a new string_view; none copy data
auto hello = text.substr(0, 5);         // "Hello"
auto world = text.substr(7, 5);         // "World"
auto end = text.substr(8);              // "World!" (C++20)

std::cout << hello << " " << world << '\n';  // OK: views still valid

Splitting lines

cpp
#include <string_view>
#include <vector>

std::vector<std::string_view> split_lines(std::string_view text) {
    std::vector<std::string_view> lines;
    while (!text.empty()) {
        auto pos = text.find('\n');
        if (pos == std::string_view::npos) {
            lines.push_back(text);
            break;
        }
        lines.push_back(text.substr(0, pos));
        text.remove_prefix(pos + 1);
    }
    return lines;  // All views point into the original text
}

// Usage
std::string file_content = ...;
auto lines = split_lines(file_content);  // zero copies
for (auto line : lines) {
    std::cout << line << '\n';
}

Constexpr string processing (C++17+)

cpp
constexpr std::string_view greeting = "Hello, World!";
constexpr auto len = greeting.size();     // 13 at compile time

// C++20: predicate functions
static_assert(greeting.starts_with("Hello"));
static_assert(greeting.ends_with("World!"));

// C++23: substring presence
static_assert(greeting.contains("lo, W"));

As an enum-to-string converter (C++20+)

cpp
enum class Status { Unknown, Created, Connected };

constexpr std::string_view to_string(Status s) {
    switch (s) {
        case Status::Unknown:   return "Unknown";
        case Status::Created:   return "Created";
        case Status::Connected: return "Connected";
    }
}

static_assert(to_string(Status::Created) == "Created");

Best Practices

Use std::string_view for read-only string parameters. It replaces const std::string& and const char* overloads in almost all cases. A single void foo(std::string_view) parameter accepts all string types with zero allocation.

Pass substrings and ranges efficiently. If you need to process a portion of a string—splitting, trimming, tokenizing—use string_view views and remove_prefix()/remove_suffix(). All operations are O(1).

Store the underlying string, not the view. If you need to retain a string as a member variable, store std::string or keep a reference to the owning object. Never store string_view as a member unless you control the lifetime of the underlying data (e.g., a static buffer or a reference to a parent).

Convert to std::string only when you need ownership. If a function returns string_view and you need to keep the result beyond the caller's lifetime, construct std::string(view) at the call site.

For C APIs, convert explicitly. Don't pass sv.data() directly to C functions expecting const char* null-terminated strings. They're incompatible: the view may not be null-terminated. Convert with std::string(sv).c_str() or use temporary buffers carefully.

Common Pitfalls

Dangling views

cpp
// DANGER: view dangling after function return
std::string_view get_name() {
    std::string local = "Alice";
    return local;  // UB – local destroyed, view is invalid
}

// DANGER: rvalue destroyed immediately
std::string_view sv = std::string("temporary");
// sv now points to freed memory

// DANGER: string reallocated, view invalidated
std::string s = "hello";
std::string_view sv = s;
s.append(" world");  // may reallocate; sv is now dangling

Lifetime must be guaranteed: the view cannot outlive the underlying data.

Data is not null-terminated

cpp
std::string_view sv = "hello world";
auto piece = sv.substr(0, 5);  // "hello"

// WRONG:
std::printf("%s\n", piece.data());  // UB – piece is not null-terminated

// RIGHT: either construct a string or use the stream operator
std::cout << piece << '\n';  // operator<< handles string_view
auto s = std::string(piece);
std::printf("%s\n", s.c_str());

Storing string_view as a class member is almost always wrong

cpp
// DANGEROUS
class Parser {
    std::string_view input;  // whose string is this? For how long?
public:
    Parser(std::string_view src) : input(src) {}
};

std::string temp = "code";
Parser p(temp);  // p.input will dangle when temp is destroyed

// SAFE – store or reference the owner
class Parser {
    std::string input;  // we own it
public:
    Parser(std::string_view src) : input(src) {}  // view is copied into owned string
};

Modification not supported

std::string_view is read-only. If you need to modify the string, use std::string& instead.

Tokenizing with remove_prefix Chaining

The most allocation-free tokenization pattern: advance a string_view in-place using find and remove_prefix. No std::vector<std::string> needed — every returned token is a view into the original buffer.

cpp
// C++17
#include <string_view>
#include <optional>

// Returns the next token up to `delim` and advances `text` past it.
// When no delimiter is found, returns the remaining text (last token).
std::string_view next_token(std::string_view& text, char delim) {
    auto pos = text.find(delim);
    if (pos == std::string_view::npos) {
        auto token = text;
        text.remove_prefix(text.size());  // exhaust the view
        return token;
    }
    auto token = text.substr(0, pos);
    text.remove_prefix(pos + 1);          // skip token + delimiter
    return token;
}

npos signals "no more delimiters" — the remaining view is the final token, so callers don't need a separate end-of-input check.

cpp
// C++17 – parse "key1=val1;key2=val2" with zero allocations
void parse_pairs(std::string_view input) {
    while (!input.empty()) {
        auto pair  = next_token(input, ';');   // one "key=val" slice
        auto key   = next_token(pair,  '=');   // key portion
        auto value = pair;                     // whatever is left

        // All three views point into the original input buffer.
        std::cout << "key=" << key << "  value=" << value << '\n';
    }
}

// Call site — one allocation: the original std::string.
std::string data = "key1=val1;key2=val2;key3=val3";
parse_pairs(data);

Zero allocation for parsing. All returned views point into the original buffer. Only one allocation ever occurs — the one that created the source string — regardless of how many tokens are extracted.

std::string_view with std::ranges::views::split (C++20)

std::views::split produces a lazy range over subranges of a string_view, deferring work until each element is consumed. This composes naturally with other range adaptors.

cpp
// C++20
#include <string_view>
#include <ranges>
#include <iostream>

int main() {
    std::string_view line = "a,b,c,d";

    // views::split returns a range of subrange objects, not string_views.
    // Wrap each subrange to get a proper string_view.
    for (auto field_range : std::views::split(line, ',')) {
        std::string_view field(field_range.begin(), field_range.end());
        std::cout << field << '\n';
    }
}

In C++20, views::split elements are std::ranges::subrange<iterator>, not std::string_view directly. The explicit construction std::string_view(r.begin(), r.end()) is necessary.

cpp
// C++20 – combine with views::transform to collect all fields
#include <string_view>
#include <ranges>
#include <vector>

auto split_csv(std::string_view line) {
    return std::views::split(line, ',')
         | std::views::transform([](auto r) {
               return std::string_view(r.begin(), r.end());
           });
}

// C++23 – views::split with string_view is directly usable; syntax simplifies
// because string_view satisfies std::ranges::contiguous_range properly.
auto split_csv_23(std::string_view line) {
    return std::views::split(line, ',')
         | std::views::transform([](std::string_view sv) { return sv; });
    // C++23: the element type is already string_view for contiguous_range inputs
}

Use views::split when composing pipelines. For simple one-pass tokenization, the manual remove_prefix loop is lower overhead; views::split shines when chaining multiple range adaptors.

Comparison and Sorting Behavior

string_view::compare and all comparison operators follow lexicographic byte order, identical to std::string. All six relational operators (==, !=, <, <=, >, >=) are defined, making string_view totally ordered.

cpp
// C++17
#include <string_view>
#include <algorithm>
#include <vector>

std::string_view a = "apple";
std::string_view b = "banana";

bool less_than = (a < b);                // true – 'a' < 'b'
int cmp        = a.compare(b);           // negative
bool equal     = (a == "apple");         // true

// Sorting a vector of views — no copies of the underlying strings
std::vector<std::string_view> words = {"zebra", "ant", "mango", "fig"};
std::sort(words.begin(), words.end());   // uses operator< — lexicographic order
// words: ["ant", "fig", "mango", "zebra"]

Case-insensitive comparison requires a comparator because string_view has no locale awareness:

cpp
// C++20 – case-insensitive sort via custom comparator
#include <string_view>
#include <algorithm>
#include <cctype>

auto iless = [](std::string_view lhs, std::string_view rhs) {
    return std::ranges::lexicographic_compare(
        lhs, rhs,
        [](char l, char r) {
            return std::tolower((unsigned char)l)
                 < std::tolower((unsigned char)r);
        });
};

std::sort(words.begin(), words.end(), iless);

All underlying strings must remain alive while views are in the sorted container. Sorting string_views rearranges pointers, not characters — the source buffers are untouched and must outlive the sorted range.

string_view in constexpr Contexts

std::string_view construction and most read operations are constexpr from C++17. find, substr, and related algorithms become constexpr in C++20/23, enabling rich compile-time string processing.

cpp
// C++17 – basic constexpr operations
constexpr std::string_view kPrefix = "Error: ";
static_assert(kPrefix.size() == 7);
static_assert(kPrefix[0] == 'E');

// C++17 – loop over characters at compile time
constexpr std::size_t count_occurrences(std::string_view text, char ch) noexcept {
    std::size_t n = 0;
    for (char c : text) n += (c == ch);
    return n;
}

static_assert(count_occurrences("hello world", 'l') == 3);

consteval functions that operate on string_view let you validate inputs or compute hashes at compile time with no runtime cost:

cpp
// C++20 – consteval: compile-time format-string validator
consteval bool is_valid_format(std::string_view fmt) {
    // Every '{' must be followed by '}'
    for (std::size_t i = 0; i < fmt.size(); ++i) {
        if (fmt[i] == '{') {
            if (i + 1 >= fmt.size() || fmt[i + 1] != '}') return false;
        }
    }
    return true;
}

static_assert(is_valid_format("Hello {}!"));
// static_assert(is_valid_format("Hello {!"));  // compile error

// C++17 – fixed-size char array round-trip
constexpr std::array<char, 5> buf = {'h','e','l','l','o'};
constexpr std::string_view from_array(buf.data(), buf.size());
static_assert(from_array == "hello");

C++26 completes constexpr support. All major implementations are expected to fully support find and rfind in constexpr contexts by C++26; C++23 already covers most practical cases.

Wide and Unicode String Views

The <string_view> header provides four additional specialisations alongside std::string_view (char):

TypeCharacter typeTypical use
std::wstring_viewwchar_tWindows WCHAR* APIs, wide literals
std::u8string_viewchar8_tUTF-8 with type-level intent (C++20)
std::u16string_viewchar16_tUTF-16 strings / u"" literals (C++17)
std::u32string_viewchar32_tUTF-32 strings / U"" literals (C++17)
cpp
// C++17 – u16 and u32 views from literals
std::u16string_view utf16 = u"Hello";   // char16_t sequence
std::u32string_view utf32 = U"Hello";   // char32_t sequence

// C++20 – u8string_view enforces UTF-8 semantic intent at the type level
std::u8string_view utf8 = u8"Hello";    // char8_t sequence

// C++17 – wstring_view for Windows APIs
std::wstring_view wide = L"Hello";

// Windows interop: wstring_view::data() is NOT null-terminated after substr
// — convert to std::wstring before passing to WINAPI functions.
std::wstring owned(wide);
::SetWindowTextW(hwnd, owned.c_str());  // safe: null-terminated

string_view has no encoding knowledge. Every operation — find, substr, size — works on individual code units (bytes for char), not Unicode code points. u8string_view is API-identical to string_view; the char8_t type merely signals UTF-8 intent at compile time and prevents accidental mixing with char strings.

Accepting string_view in Generic Code

When writing templates or library interfaces, std::string_view is the preferred non-owning parameter type. It avoids the overload proliferation of const char* / const std::string& and is the only single-type parameter that accepts all string-like arguments without allocation.

cpp
// C++17 – string_view beats const string& for read-only parameters
void log_v1(const std::string& msg);   // forces allocation for string literals
void log_v1(const char* msg);          // requires second overload

void log_v2(std::string_view msg);     // accepts all of the above — zero alloc

const std::string& does not accept std::string_view — the implicit conversion goes the other way (string_view from string, not string from string_view). A string_view parameter is therefore the only way to accept both string and string_view without overloads.

cpp
// C++20 – concept-constrained generic function
#include <concepts>
#include <string_view>

template <std::convertible_to<std::string_view> S>
void log(S&& msg) {
    log_impl(std::string_view(std::forward<S>(msg)));  // one view, no allocation
}

log("literal");                  // const char* → string_view, zero alloc
log(std::string("owned"));       // string → string_view, zero alloc
log(std::string_view("view"));   // already a view, trivial

Pass string_view by value, not by const reference. A string_view is 16 bytes (pointer + size) on all major platforms — smaller than a pointer-to-string indirection. void foo(std::string_view sv) is preferred over void foo(const std::string_view& sv).

cpp
// C++17 – correct: by value
void process(std::string_view sv);        // preferred

// C++17 – redundant indirection: by const ref
void process(const std::string_view& sv); // adds an unnecessary pointer dereference

See Also

  • std::string — owning string class
  • std::span — equivalent for any contiguous container (C++20)
  • std::format — formatted output with string_view support (C++20)