std::string_view by Value: The C++17 Read-Only String Parameter, and Where It Bites
Pass read-only string parameters as std::string_view by value: what the pointer-plus-length handle buys you, where NUL termination and lifetime break it, and how to check your build.
28 Oct 2025, 10:08 UTC

Use std::string_view by value for read-only string parameters
If a function only reads its string argument, declare the parameter as std::string_view and take it by value. That is the C++17 answer to a question that used to have two mediocre answers: const std::string& and const char*.
const std::string& forces a std::string to exist at the call site. Passing a literal or a char* constructs a temporary std::string, which may allocate — short strings often fit in the small-string buffer, so the cost is not guaranteed, but the type still imposes an owning container on a read-only operation. const char* avoids the container but throws away the length, so the callee must call strlen and cannot accept a slice or a buffer containing embedded NUL bytes.
std::string_view is a non-owning handle: a pointer plus a length. It binds implicitly to string literals, std::string, character arrays, and explicit pointer-plus-length slices. Nothing is copied and nothing is allocated at the call site.
What the handle actually contains
A std::string_view stores a const char* and a size. On a 64-bit platform it is typically two words (16 bytes), though the exact layout is an implementation detail. Copying a view copies two words; it does not copy characters. Members such as size, substr, compare, find, remove_prefix, and remove_suffix operate on the pointer-length pair. In particular, substr returns another view into the same buffer in constant time — it does not allocate, unlike std::string::substr.
Worked example: a hex-prefix check
This function accepts any read-only string source without copying:
#include <cctype>
#include <string>
#include <string_view>
// Returns true if s is "0x" followed by one or more hex digits.
bool is_hex(std::string_view s) {
if (s.size() < 2 || s.substr(0, 2) != "0x") return false;
for (char c : s.substr(2)) {
if (!std::isxdigit(static_cast<unsigned char>(c))) return false;
}
return true;
}
Calling it:
std::string cfg = load_config(); // placeholder: whoever owns the bytes
bool a = is_hex("0xdeadbeef"); // string literal
bool b = is_hex(cfg); // std::string -> view, no copy
bool c = is_hex(std::string_view(buf + 4, 10)); // slice, no copy
All three call sites reach the same function. is_hex(cfg) converts the std::string to a view through its implicit conversion operator; the characters stay in cfg's buffer. The pointer-plus-length form requires buf to point at a buffer with at least 14 readable bytes — the view does not check that, and a wrong length is a bug the compiler cannot catch.
Inside the function, s.substr(0, 2) and s.substr(2) are cheap views, and the loop reads characters directly from the caller's buffer. std::isxdigit takes an int, and passing a plain char can be undefined behavior for negative values, hence the unsigned char cast.
Where the abstraction breaks
No NUL-termination guarantee
A view says nothing about a terminator. data() may point into the middle of a longer buffer, and the byte after data() + size() is not necessarily '\0'. Passing sv.data() to a C interface such as fopen or printf("%s", ...) makes that interface read past the end of the view. Copy into an owning string first when a terminator is required: std::string tmp(sv); and then use tmp.c_str().
Borrowed lifetime
The view does not own its characters. The buffer must outlive every use of the view. Returning a view to a local std::string compiles and is undefined behavior:
std::string_view bad() {
std::string local = "0xabc";
return local; // view into a buffer destroyed at return
}
The same hazard appears when a view is stored in a struct, a container key, or a cache while the source string is later mutated or destroyed. Reallocating a std::string invalidates views into it. A related trap is binding a view to a temporary: a function that stores its std::string_view argument will hold a dangling view if the caller passes a temporary std::string, because the temporary dies at the end of the full expression.
No concatenation between two views
operator+ is not defined for two std::string_view operands, because the result would need an owner. Concatenation requires at least one std::string.
Version-dependent members
The type itself is C++17. Convenience members arrived later, so the language standard you compile with determines what exists:
| Member | Available from |
|---|---|
substr, compare, find, remove_prefix, remove_suffix | C++17 |
starts_with, ends_with | C++20 |
contains | C++23 |
If your build is pinned to C++17 and you want starts_with, write a small helper using substr or compare rather than raising the standard for one call. Raising the standard also changes other library behavior, so treat it as a project-wide decision.
Common mistakes
- Returning a
std::string_viewthat points into a localstd::string. - Storing views as struct members, cache entries, or associative-container keys while the referenced strings can be mutated or destroyed.
- Constructing a view from a
char*with a guessed length instead of a measured one. - Assuming
substrcopies, asstd::string::substrdoes. On a view it aliases the original buffer. - Assuming
data()is NUL-terminated and handing it to a C API. - Using a view where the callee actually needs to own the characters.
Checking the decision in your own build
- Compile the example with
-std=c++17(GCC/Clang) or/std:c++17(MSVC) from a shell in your project directory. No special permissions are needed. Confirm it accepts a literal, astd::string, and a pointer-plus-length slice. - Reproduce the dangling case in a scratch file and build it with
-fsanitize=address -g. AddressSanitizer is designed to report stack-use-after-scope for this pattern; the exact message text varies by compiler and version, so treat the presence of a report as the signal, not its wording. - Check your standard library's reference (libstdc++, libc++, or the MSVC STL) for which view members exist at your chosen standard level.
- If the goal is eliminating allocations, confirm it with a profiler or a custom allocator that counts heap calls. Do not assume the change removed them.
When to keep const std::string& or return std::string
Return std::string (or std::optional<std::string>) whenever the caller needs to own the result. Return a view only when it clearly points into an argument or other storage that outlives the call — a trim that returns a sub-view of its parameter is the canonical safe case. Keep const std::string& when the callee stores a reference, or when the call site already has a std::string and no conversion is happening anyway. If a toolchain cannot compile C++17, the fallback is mechanical: change the parameter type back to const std::string& and rebuild.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.