Skip to content

64 Intermediate The Hundred

The C String From a Dead String

the greeting that only works for people with short names

struct User {
    std::string first;
    std::string last;
    std::string getName() const { return first + " " + last; }
};

void greet(const char* name) { std::printf("Welcome, %s!\n", name); }

int main() {
    User intern{"Ada", "King"};
    User founder{"Augusta Ada", "Byron King, Countess of Lovelace"};

    const char* name = intern.getName().c_str();
    greet(name);

    name = founder.getName().c_str();
    greet(name);
}

Run it. Who gets greeted?

Answer

Only the intern. Typical output (GCC 11.5 on x86-64, glibc 2.34): Welcome, Ada King!, then Welcome, plus two junk bytes and a ! — different bytes nearly every run, occasionally a newline that splits the line. Undefined behavior, including the line that worked.

Why

c_str() is the border crossing into C, and it hands you the type C uses for every string: a bare const char* — no length, no ownership, nothing that whispers borrowed. So it looks as storable as an int, and you park it in a variable or a struct field before feeding it to printf, open, strcmp. Its lifetime is really the string's, and that string is a temporary here — getName() returns by value — dead at the closing semicolon, so every later read is undefined behavior. The founder's 44-character name sat on the heap, and freeing it let glibc write a mangled free-list pointer over the first bytes (the junk); "Ada King" is 8 characters, short enough for the small-string optimization to keep it inside the temporary itself (libstdc++ inlines up to 15 — implementation-defined), where the dead bytes still read correctly.

GCC 11.5 and 13.3 stay silent even with -Wall -Wextra; Clang 18 needs no flags — object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl] — but flags only the initialization, never the identical assignment below. -fsanitize=address aborts on the first greet with stack-use-after-scope: that line was never safe either.

The fix

std::string name = founder.getName();   // the characters are yours now
greet(name.c_str());                    // valid for as long as `name` is

Give the characters a named owner and cross into C at the point of use. Handing one straight over, greet(founder.getName().c_str()), is fine too — the temporary outlives the call. Entry 65 is the same wound from std::string_view, which at least admits that it borrows.

Takeaway: c_str() lends you a pointer on the string's lifetime — const char* never says so.

Try it: g++ -std=c++17 main.cpp -o demo && ./demo

Open in Compiler Explorer ↗ Quiz this entry