top of page

A Tiny C++ Detail That Can Crash Your System

  • Writer: alex d
    alex d
  • Dec 22
  • 2 min read
ree

When we talk about modern C++, it’s easy to focus on advanced topics: coroutines, async frameworks, schedulers, and performance tuning.But time and again, real production bugs come from something much simpler.

Fundamentals.

In this post, I want to show how a single line that looks completely reasonable can introduce a use-after-free bug in coroutine-based code and why understanding ownership and lifetimes is more important than ever in modern C++.


The Code

Consider the following snippet.Don’t worry about the concrete framework or implementation details — assume the functions involved are coroutines and that execution is preemptive.

Future<void> set_client_routes(const SpecialObj& obj) {
    return execute_serially(
        [obj = std::move(obj)] (Provider& provider) {
            return provider.with_retry_co_routine([&] {
                return provider.update(obj);
            });
        }
    );
}

At first glance, nothing looks suspicious. In fact, many experienced C++ developers would read this and move on.

Let’s slow down and analyze what’s really happening.


What the Code Is Trying to Do

The intent is reasonable and common in async code:

  1. The public API takes obj as a const reference→ cheap, caller-friendly, no ownership transfer.

  2. The outer lambda “moves” obj into the async boundary→ ensuring safe lifetime across asynchronous execution.

  3. An inner coroutine uses obj to update some internal state.

So far, so good.

Or so it seems.


The Subtle Problem

The key line is this one:

[obj = std::move(obj)]

Here’s the catch:

You cannot move from a const object.

Because obj is const SpecialObj&, std::move(obj) does not move anything. It simply casts obj to const SpecialObj&&.

And since move constructors typically require a non-const rvalue, the result is:

👉 A copy, not a move.

This detail is easy to miss and extremely dangerous in async code.


Why This Becomes a Bug

Let’s look at the lifetimes involved:

  • The copied obj lives inside the outer lambda

  • The outer lambda starts a preemptive coroutine

  • That coroutine may suspend

  • The outer lambda can finish execution

  • The lambda (and its captured obj) are destroyed

  • Later, the coroutine resumes

  • The inner lambda accesses obj by reference

At this point, obj is already gone.

🎯 Result: use-after-free

In the best case, this causes undefined behavior. In the worst case, it causes a hard crash in production far away from where the mistake was made.


Why This Is So Dangerous

What makes this bug particularly nasty is that:

  • The code looks correct

  • The intent is good

  • There are no raw pointers

  • There are no obvious lifetime violations

  • The failure is timing-dependent

This is not a framework issue. This is not a coroutine issue.

This is pure C++ semantics.


The Real Lesson

Async code magnifies lifetime mistakes.

A small misunderstanding around:

  • const

  • move semantics

  • lambda captures

  • coroutine suspension

…can turn into a production crash that’s nearly impossible to debug.

That’s why C++ fundamentals are not “basic knowledge you learn once”.They are the foundation that modern, high-level C++ is built on.

Takeaway

If ownership is unclear at an async boundary,your program is already broken — it just doesn’t know it yet.

Understanding when objects live, who owns them, and how they cross async boundaries is what separates “working code” from correct code.

Happy learning and never underestimate the basics.

 
 

Recent Posts

See All
bottom of page