Strange behavior when lambda uses `this` vs "outside instance"

47 Views Asked by At

I'm trying to keep my code well-organized as I learn C++ for Arduino (specifically Nano in my case). There's a function attachInterrupt() that allows me to bind a hardware interrupt to a specific pin when it undergoes a state change.

Signature: void attachInterrupt(uint8_t interruptNum, void (*userFunc)(void), int mode)

So far, so good. However, I've created a class to manage a specific aspect of my program. This way, I can reuse the code instead of redoing the setup() multiple times.

So, imagine I have a certain type of digital sensor and I'm going to use attachInterrupt() for it. Normally, I would write the following code:

#include <Arduino.h>

unsigned int count = 0;

void countISR() {
  count++;
}

void setup() {
  pinMode(PIN2, INPUT);
  attachInterrupt(digitalPinToInterrupt(PIN2), countISR, CHANGE);
}

void loop() {
  Serial.println("Count = " + String(count));
  delay(1000);
}

This tends to work very well. However, as mentioned, I'd like to better organize my code and allow for code reuse for various interrupt-supporting pins (or even using libraries like EnableInterrupt, but that's beside the point).

Therefore, I created a class called Counter:

class Counter {
  public:
    uint8_t pin;
    unsigned int count = 0;

    Counter(uint8_t pin): pin(pin) {}

    void setup() {
      pinMode(pin, INPUT);
      // attachInterrupt() should go here.
    }

    void update() {
      count++;
    }
};

This way, I can instantiate multiple independent Counters, something like:

Counter counterA(PIN2);
Counter counterB(PIN3);

void setup() {
  counterA.setup(); // Set pinMode() and interrupt to PIN2.
  counterB.setup(); // Set pinMode() and interrupt to PIN3.
}

However, when trying to perform an attachInterrupt() inside Counter::setup(), I need to create a C++ Lambda, and I would typically do something like:

void Counter::setup() {
  // pinMode(...)
  attachInterrupt(pin, []() { update(); }, CHANGE);
}

In theory, this should be enough. But C++ indicates that I can't do it this way, issuing the following error:

Error: the enclosing-function 'this' cannot be referenced in a lambda body unless it is in the capture list. C/C++ (1738)

Alright, so let's try putting this in the capture list:

void Counter::setup() {
  // pinMode(...)
  attachInterrupt(pin, [this]() { update(); }, CHANGE);
}

And now there's another error:

Error: no suitable conversion function from "lambda ->void" to "void (*)()" exists. C/C++(413)

I understand the reason for the error, as it conflicts with the signature of attachInterrupt(). However, if I do this within the main void setup(), it will work:

void setup() {
  // pinMode(...)
  attachInterrupt(PIN2, []() { counterA.update(); }, CHANGE);
}

In other words, one way or another, I'm passing an instance into a lambda without even needing to capture it explicitly.

So I thought: maybe the problem is the use of this, so I tried assigning it to a variable first, like:

void Counter::setup() {
  // pinMode(...)

  const auto self = this;
  attachInterrupt(pin, []() { self.update(); }, CHANGE);
}

The error changes, but it's practically the same as the first one:

Error: an enclosing-function local variable cannot be referenced in a lambda body unless it is in the capture list. C/C++(1735)

This left me quite confused: why is it allowed to do this outside of a class, but inside it, I need to go through this capture process?

To solve the problem for now, I had to make an adjustment that I found ugly:

void setup() {
  counterA.setup([]() { counterA.update(); })
}

Thus, in Counter::setup(), I receive a lambda in the format allowed by attachInterrupt(), already capturing counterA or counterB without problems.

So, what am I doing so wrong?

0

There are 0 best solutions below