C++'s std::string_view from std::string::substr: The View Into a Vanishing Temporary

2026-06-15

This function is supposed to return the file extension of a path as a cheap, non-owning view. It compiles clean under -Wall -Wextra -Wpedantic, passes every test in the unit suite, and ships to production. A week later, your log parser starts emitting filenames like "\x7f\x00\x00\x00g\xff" instead of "log".

#include <string>
#include <string_view>
#include <iostream>

std::string_view file_extension(const std::string& path) {
    size_t dot = path.rfind('.');
    if (dot == std::string::npos) return {};
    return path.substr(dot + 1);
}

int main() {
    std::string p = "/var/log/application.logfile";
    auto ext = file_extension(p);
    std::cout << "ext=" << ext << "\n";
    return 0;
}

Run it. On many machines it prints ext=logfile — exactly what you wanted. Run it under ASan, or with a longer suffix, or in a slightly different build, and it prints garbage or crashes.

The Bug

The signature lies. file_extension claims to return a std::string_view — a borrowed pointer plus a length. But look at the return expression: path.substr(dot + 1). Critically, std::string::substr does not return a view into the original string. It returns a brand-new std::string by value — a temporary that owns its own heap buffer.

That temporary is then implicitly converted to std::string_view. The view records a pointer into the temporary's storage. The temporary is destroyed at the end of the full expression (the return statement). The caller receives a string_view pointing into freed memory. Classic use-after-free.

So why does "logfile" sometimes appear correctly? Small String Optimization. Short strings live inside the std::string object itself, not on the heap. When the temporary dies, its inline buffer's bytes happen to remain on the stack — undisturbed until the next function call clobbers them. The bug is invisible for suffixes under ~15 characters (libstdc++) or ~22 (libc++), and only surfaces when the extension is long enough to force a heap allocation. The shorter your test cases, the longer the bug hides.

The compiler can warn about this with -Wdangling on recent GCC/Clang, but only because std::string_view is marked [[gsl::Pointer]] in the standard library headers. Many older toolchains still emit nothing.

The Fix

Take a string_view in, return a string_view out. std::string_view::substr returns another string_view — no allocation, no temporary, no lifetime extension games:

std::string_view file_extension(std::string_view path) {
    size_t dot = path.rfind('.');
    if (dot == std::string_view::npos) return {};
    return path.substr(dot + 1);  // string_view::substr returns string_view
}

The body looks identical. The difference is the parameter type: now substr resolves to the string_view overload, which slices the existing view in place rather than allocating a new owning string. The returned view's lifetime is whatever the caller's input was bound to — the function itself introduces no temporaries.

If you genuinely need ownership, return std::string and accept the allocation. The dangerous middle ground is a non-owning return type produced from an owning intermediate.

Key Takeaway: Any function that returns a string_view, span, or other borrowed handle must verify the borrow points at a parameter or longer-lived storage — never at a temporary created inside the function, because Small String Optimization will let the bug pass every short-input test you write.

All newsletters