DEV Community

Cover image for 取得指向類別成員函式的指位器
codemee
codemee

Posted on • Edited on

1

取得指向類別成員函式的指位器

在開發 ESP32 的應用程式時, 有時候會需要取得指向函式的指位器, 像是要設定中斷的話, 就必須提供一個函式作為中斷觸發時的處理常式, 如果採用一般的函式, 只要直接寫函式名稱就可以了, 例如這個程式

int count = 0;

void ARDUINO_ISR_ATTR isr(void) {
  count++;
}


void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(26, INPUT_PULLUP);
  attachInterrupt(26, isr, RISING);
}

void loop() {
  // put your main code here, to run repeatedly:
  delay(200); // this speeds up the simulation
  Serial.println(count);
}
Enter fullscreen mode Exit fullscreen mode

但如果你想要使用類別中的成員函式當成中斷處理常式, 直接這樣寫會出錯:

class Button {
  public:
    int count = 0;

    void ARDUINO_ISR_ATTR isr(void) {
      count++;
    }

};

Button b;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(26, INPUT_PULLUP);
  attachInterrupt(26, Button::isr, RISING);
}

void loop() {
  // put your main code here, to run repeatedly:
  delay(200); // this speeds up the simulation
  Serial.println(b.count);
}
Enter fullscreen mode Exit fullscreen mode

它會說你用錯非靜態的類別成員函式:

sketch.ino: In function 'void setup()':
sketch.ino:17:31: error: invalid use of non-static member function 'void Button::isr()'
   attachInterrupt(26, Button::isr, RISING);
                               ^~~
Enter fullscreen mode Exit fullscreen mode

如果改成透過實例, 也一樣會出錯:

  attachInterrupt(26, b.isr, RISING);
Enter fullscreen mode Exit fullscreen mode

如果改成這樣想要取得成員函式的位址:

  attachInterrupt(26, &b.isr, RISING);
Enter fullscreen mode Exit fullscreen mode

它會告訴你不允許取得實例的成員函式位址, 也沒辦法幫你把成員函式轉換成一般函式:

sketch.ino: In function 'void setup()':
sketch.ino:17:26: error: ISO C++ forbids taking the address of a bound member function to form a pointer to member function.  Say '&Button::isr' [-fpermissive]
   attachInterrupt(26, &b.isr, RISING);
                          ^~~
sketch.ino:17:37: error: cannot convert 'void (Button::*)()' to 'void (*)()'
   attachInterrupt(26, &b.isr, RISING);
                                     ^
Enter fullscreen mode Exit fullscreen mode

實際上成員函式就跟一般函式是一樣的, 只不過叫用成員函式的時候, C++ 編譯器會幫你把綁定的實例傳入成為 this 指向的物件。如果你寫過 Python, 對這一點就會很有感。

為了解決這個問題, C++ 提供有 std::bind 函式, 只要傳入類別中成員函式的位址, 以及要綁定在哪一個物件上叫用它, 它就會幫你建立一個 std::function 類別的物件, 並且紀錄成員函式的位址與物件, 當你叫用這個 std:function 類別實例時, 它會幫你轉去叫用成員函式, 並且把記錄的物件傳入。

ESP32 就搭配這個機制, 另外在 FunctionalInterrupt.h 檔中提供了相對應版本的 attachInterrupt 函式, 剛剛的程式就可以改寫如下的版本

#include<FunctionalInterrupt.h>

class Button {
  public:
    int count = 0;

    void ARDUINO_ISR_ATTR isr(void) {
      count++;
    }

};

Button b;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(26, INPUT_PULLUP);
  attachInterrupt(26, std::bind(&Button::isr, &b), RISING);
}

void loop() {
  // put your main code here, to run repeatedly:
  delay(200); // this speeds up the simulation
  Serial.println(b.count);
}
Enter fullscreen mode Exit fullscreen mode

要注意:

  1. 一定要引入 FunctionalInterrupt.h, 否則會引用到原始版本的 attachInterrupt 函式, 這個版本只接受 voidFuncPtr 型別的函式, 它實際上是 void (*voidFuncPtr)(void);, 不接受 std::function 類別實例, FunctionalInterrupt.h 內的版本 才能接受 std::function 類別實例。

  2. 叫用 bind 時記得傳入物件的位址, 否則會變成複製物件, 如果在中斷處理函式中會修改物件的成員變數, 就會變成修改到副本, 而不是你傳入的那個物件。

💡 One last tip before you go

Spend less on your side projects

We have created a membership program that helps cap your costs so you can build and experiment for less. And we currently have early-bird pricing which makes it an even better value! 🐥

Check out DEV++

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more