DEV Community

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

Posted on • Updated on

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

在開發 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 時記得傳入物件的位址, 否則會變成複製物件, 如果在中斷處理函式中會修改物件的成員變數, 就會變成修改到副本, 而不是你傳入的那個物件。

Top comments (0)