ThatOneGuyLogan
ThatOneGuyLogan

Reputation: 63

Reference captured by C++ lambda has different value when lambda executes

I'm building a pubsub system for a project and I've got an interesting problem with event values changing between notification and dispatch of events. The event I'm seeing the problem in is a MouseMoveEvent which contains screen coordinates using glm::vec2() (it has double-typed x and y fields).

template<class E>
void notify_event(std::shared_ptr<E> event) {
  if (_auto_dispatch) {
    dispatch<E>(event);
  } else {
    // inspecting values here gives expected coordinates: 
    // like: 168.684,284.547
    debug<E>(event); 
    _dispatch_queue.push([&]() {
      // when the lambda runs, the coordinates are major messed up
      // like: -1.82767e+16,4.57398e-41
      debug<E>(event); 
      dispatch<E>(event);
    });
  }
}

The -1.82767e+16,4.57398e-41 coordinates are given regardless of input values. I initially used references to pass around events, then tried shared pointers, but I get exactly the same results for both. Dispatching events immediately works without any problems, but my project requires me to collect events and then dispatch after later because different systems will be in various states of "ready" as events are created.

I feel like the captured event reference ends up pointing at garbage memory somehow, but I don't understand lambdas in C++ well enough to know why I'm seeing this.

What's going on and what are options to get around this?

Upvotes: 0

Views: 290

Answers (2)

Jerry Coffin
Jerry Coffin

Reputation: 490108

A C++ lambda is basically a shortcut way to define a class. The class always defines an operator(), and if the lambda doesn't capture anything, also defines conversion to pointer to function (taking the same parameters/returning the same type as operator().

The capture list gets turned into parameters that are passed to that class' constructor. So, if it captures something by reference, it captures a reference to that particular variable as it exists when the lambda is first encountered. So, if you have something like this:

auto foo = [&] { 
    ++some_captured_variable.bar;
};

...it's really equivalent to something like this:

class some_unknown_name {
    T &some_captured_variable;
public:
    operator()() { ++some_captured_variable.bar; }

    some_unknown_name(T &c) : some_captured_variable(c) {}
};

auto foo = some_unknown_name(some_captured_variable);

So, it captures the reference when you create the object, but doesn't use it until you execute the lambda.

In this case, the object whose reference you captured has apparently gone out of scope by the time you try to actually execute the lambda--so you're using a stale reference, which doesn't work well.

Upvotes: 0

Some programmer dude
Some programmer dude

Reputation: 409166

It seems that the lambda will be called after the notify_event function returned, which means the argument event will have gone out of scope and the reference will be invalid.

While there certainly are use-cases where a shared pointer should be passed (or captured) by reference, this doesn't seem to be one of those cases. Capture it by value so that the lambda will have its own shared pointer that will always be valid (even after the otherwise last instance have been destructed).

Upvotes: 7

Related Questions