A Tiny C++ Detail That Can Crash Your System
- alex d
- Dec 22
- 2 min read

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:
The public API takes obj as a const reference→ cheap, caller-friendly, no ownership transfer.
The outer lambda “moves” obj into the async boundary→ ensuring safe lifetime across asynchronous execution.
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.
