[Debug Series] Don't capture reference unless it's transient

发布时间 2024-01-07 17:14:29作者: sherlock2001

Lambda is such a powerful thing and supported by many programming languages. Unlike the one in C#, lambda expression in C++ requires much more dedicated considerations, the capture of course, is one amoung which.

The capture itself is not a obsure concept. A capture is a mechanism used in lambda expressions to capture variables from the surrounding scope. Here's one example:

int i{ 123 };

auto lambda = 
  [ i/*i was captured along with this lambda instance*/ ] {
    std::cout << i << std::endl;
  };
lambda(); // '123' will be printed

Life is good until capture by reference comes. To capture by reference, simply put a & prefix to the target capture member. Here's one example.

int i{ 123 };

auto lambda = 
  [ &i/*i was captured by reference, so that we save a copy*/ ] {
    std::cout << i << std::endl;
  };
lambda(); // '123' will be printed

As the comment states, capture by reference can prevent us from having to copy values blindly. But here comes the problem, what if this lambda instance was invoked somewhere else outside of scope? The captured reference '&i' will be invalid, the program will enter undertermined state.

That's the reason why before acutally capturing by reference, we have to consider the fact that if it's a transient invocation (in other words, be invoked immediately at current scope), or it's a latent invocation.

Here's a real world simplified example I encountered just now. The background is that I am implementing a fetch more feature in some sort of a list widget in for my game using Unreal Engine. I have a timer which ticks at an interval of 0.25 seconds, and during each tick a callback named OnTimerTick will be raised; Inside this callback I will send a fetch more request and utilize the response to update the widget.

void OnTimerTick(Widget InWidget) {
  auto FetchRequest = FetchMoreRequestBuilder()
                      // notice here we capture by reference
                      .OnResponse([&InWidget](const FetchMoreResponse& Rsp)
                                  { Widget.Update(Rsp); })
                      .Build();
}

At first glance it's hard to spot the problem. The problem occurs if the lambda passed to OnResponse is invoked after the widget's deconstruction, and during which the captured &InWidget has already been freed, an exception will occur and the program refuses to continue its execution.