← Article list

A Static Lambda in C++ Can Keep Using the First Input

This article analyzes why unordered_map::find can return the expected key while the value is updated from the wrong input. A function-local static lambda is initialized only once, so its reference capture continues to refer to the object bound during the first call. Later calls may therefore reuse stale data; accessing the object after its lifetime ends is what creates a dangling-reference error and undefined behavior. Pass the current input as an explicit lambda parameter instead.

On this page

When building high-performance market data services or distributed caches, developers may encounter a “ghost event”: std::unordered_map finds the expected key, but the value fields look as if they came from another key. One possible cause is a static lambda that keeps using the first call’s input object.

The First Call’s Input Can Be Reused

When optimizing performance, some developers are accustomed to defining static lambda functions within a function to reduce closure creation overhead. However, when combined with implicit capture using [&], this can lay a dangerous trap:

void UpdateCache(std::unordered_map<std::string, Tick>& cache, Tick& current_input) {
    // Dangerous: static extends the lambda's lifetime to process level
    // [&] keeps referring to the Tick object bound on the first call
    static auto patch_func = [&](auto it) {
        it->second.price = current_input.price; // Later calls still read the first input object
    };

    auto it = cache.find(current_input.symbol);
    if (it != cache.end()) {
        patch_func(it); 
    }
}

Technical Principle Analysis

  1. Capture target: The C++ rules say that using a reference captured by reference refers to the entity that reference is bound to, not to the reference variable itself. The closure therefore keeps referring to the first call’s Tick object; it does not rebind on later calls. C++ working draft: Lambda captures
  2. Stale input or dangling access: If the first Tick object is still alive, the lambda reads old fields. If that object has been destroyed, accessing it is undefined behavior.
  3. Misleading symptom: The map lookup can still find key A while the update copies fields from the first input object. This can make the value appear to belong to key B without the container lookup itself being wrong.

Solutions and Engineering Practices

1. Eliminate Implicit Capture, Use Explicit Parameters

The most reliable approach is to keep the Lambda “stateless” (Stateless), passing all dependent objects through parameters.

// Recommended practice: Stateless lambda, explicitly pass src
auto patch = [](auto it, const Tick& src) {
    it->second.price = src.price;
};
patch(it, current_input);

2. Do not store call-specific captures in a static closure

Do not store a lambda that depends on the current call’s input as a function-local static. If the update logic should be reused, keep the lambda stateless and pass the current input as a parameter.

3. Reinforcement Consistency Verification and Dynamic Detection

  • Assertion Checking: After updating the logic, add assert(it->first == it->second.symbol) to intercept “identity inconsistency” issues during development.
  • Tool Assistance: Enable ASan (AddressSanitizer) and UBSan (UndefinedBehaviorSanitizer) in the test environment. These tools can accurately capture behavior of accessing invalid stack memory and immediately report errors.

Conclusion

“Table lookup errors” in C++ can be symptoms of stale inputs or broken lifetime assumptions. A correct key lookup does not prove that the update used the current input. When a long-lived closure captures by reference, check which object it refers to and whether that object outlives every use.

Table of contents

Comments

Comments will load when you scroll here.