Callback mit class member Funktionen

Hallo,

ich habe mir die Lib ModbusMaster installiert und wollte die Ansteuerung dieser Lib in einer eigenen Klasse/Lib unterbringen. Damit ich die Code-Teile sauber voneinander getrennt bekommte.

Jetzt habe ich das Problem, das die ModbusMaster Lib zwei Callback-Funktionen verwendet

void ModbusMaster::preTransmission(void (*preTransmission)())
{
  _preTransmission = preTransmission;
}

void ModbusMaster::postTransmission(void (*postTransmission)())
{
  _postTransmission = postTransmission;
}

In diesen Callback-Funktionen muss ich ein paar Pins ansteuern. Da ich die gesamte Steuerung der Modbus-Lib in meiner eigenen Klasse unterbringen möchte, muss ich auch diese Callback-Funktionen aus meiner Klasse herraus bedienen.

Dazu habe ich zwei Methoden erstellt.

void TestConnector::preTransmission(){
	digitalWrite(_max485REPin, 1);
	digitalWrite(_max485DEPin, 1);
}

void TestConnector::postTransmission(){
	digitalWrite(_max485REPin, 0);
	digitalWrite(_max485DEPin, 0);
}

Die Variablen _max485REPin und _max485DEPin sind in meiner Klasse definiert und werden über den Konstruktor mit Werten belegt.

Sobald ich aber meine Funktionen für das Callback verwenden möchte, bekomme ich folgende Fehlermeldung. "error: invalid use of non-static member function"

ModbusMaster _node;

[...]

_node.preTransmission(TestConnector::preTransmission);
_node.postTransmission(TestConnector::postTransmission);

Meine Frage ist jetzt, wie kann ich meine beiden Klassen-Methoden für ein Callback verwenden?

Ein Callback auf eine Funktion und eine Methode sind zwei grundlegend verschiedene Sachen, da Methoden durch den this-Zeiger eine andere Signatur haben

Was als Workaround in manchen Situationen (vor allem wenn es nicht mehrere Objekte der Klassen gibt) geht ist in eine anonyme Lambda Funktion die einen statischen Zeiger auf das Objekt verwaltet:

class OtherClass    //Klasse die intern den Callback verwaltet. Nur als Test
{
public:
  void setFunction(void(*callBack)(void))    //Callback setzen
  {
    func = callBack;
  }

  void callFunction()     //Callback-Funktion aufrufen. Das wäre normale private und intern, aber hier muss ich es per Hand machen
  {
    Serial.println("callback");
    if (func != NULL)
      func();
  }
private:
  void(*func)(void);    //Zeiger auf Callback
};

class MyClass
{
public:
  MyClass() : otherClass()   //Konstruktor
  {
    static MyClass* obj = this;   //Zeiger auf das Objekt

    otherClass.setFunction(       //Funktionszeiger über anonyme Lambda Funktion setzten
      []() { obj->doSomething(); }
    );

    otherClass.callFunction();      //nur zum Test manuell aufrufen damit man was sieht!
  }

  void doSomething()
  {
    Serial.println("Do something");
  }
private:
  OtherClass otherClass;
};


void setup()
{ 
  Serial.begin(9600);

  MyClass obj;
}

void loop()
{
}

Beachte dass ich den Callback nur zum Testen per Hand aufrufe. In der Realität würde das intern durch die andere Klasse geschehen. Auch dass das im Konstruktor ist, ist nur zum Test damit es gleich beim Erstellen des Objekts gemacht wird und man nicht noch mehr Methoden braucht

Die eigentliche Magie ist nur dieser Teil:

static MyClass* obj = this;

otherClass.setFunction(
  []() { obj->doSomething(); }
);

Man kann auch eine statische Wrapper Methode schreiben (die dann keinen this Zeiger hat, weil sie keinem Objekt zugeordnet ist), aber das ist mehr Aufwand

Die einfachste Lösung dürfte sein, den Typ der Callback-Funktionen so zu ändern, daß es Methoden sein müssen. Die Details hierfür überlasse ich den C++ Gurus, ich kenne mich nur mit OPL genauer aus.

DrDiettrich:
Die einfachste Lösung dürfte sein, den Typ der Callback-Funktionen so zu ändern, daß es Methoden sein müssen

Das ist nicht einfach, weil der Callback dann nur Methoden auf diese eine Klasse verwalten kann

Die STL enthält Dinge mit denen man das elegant umgehen kann. Siehe std::bind. Auf den ESPs geht das auch. Aber auf den AVRs natürlich nicht

Ich kann nicht glauben, daß es etwas gibt, das in C++ nicht geht, obwohl es in Delphi funktioniert :-]

Zumindest mit einer eigenen Callback-Klasse sollte das doch gehen.

Es geht ja. Man muss nur etwas ausholen. Der Trick besteht darin, dass eine statische Methode keinen this Zeiger hat. Damit kann ein Funktionszeiger auf eine statische Methode zeigen. Außerdem sind Lamdafunktionen mit leerer Capture Liste direkt in Funktionszeiger konvertierbar.

Gerade im Arduino Bereich geht das oft, weil viele Objekte Singeltons sind

Statische Methoden sind hier doch nur unpraktisch, weil sie keinen Bezug zu einer Instanz ihrer Klasse haben. Sie können also nicht mehr als ordinäre Funktionen.

Es kommt auf die Anwendung an. Die Klasse hier sieht wie so oft stark nach einem Singleton aus. Dann ist es egal. Weil es sowieso nicht mehrere Instanzen gibt

Serenifly:
Die eigentliche Magie ist nur dieser Teil:

static MyClass* obj = this;

otherClass.setFunction(
  { obj->doSomething(); }
);

Vielen Dank für den Tipp. Das sieht schon mal richtig gut aus :wink:

DrDiettrich:
Statische Methoden sind hier doch nur unpraktisch, weil sie keinen Bezug zu einer Instanz ihrer Klasse haben. Sie können also nicht mehr als ordinäre Funktionen.

Serenifly:
Es kommt auf die Anwendung an. Die Klasse hier sieht wie so oft stark nach einem Singleton aus. Dann ist es egal. Weil es sowieso nicht mehrere Instanzen gibt

Wie würde die Lösung aussehen, wenn die Klasse nicht singelton wäre?

Es geht meines Wissens nicht. Ein Funktionszeiger hat einfach keinen Bezug zu einem Objekt.

Du kannst die Modbus Klasse so ändern dass sie einen Zeiger auf ein Methode als Parameter hat. Dann geht sie aber nicht mehr universell, sondern nur mit der Klasse. Das kann aber für ein bestimmtes Projekt auch ok sein.

Das ist wie gesagt eher ein Arduino Problem. In Standard C++ könnte man die Klasse so schreiben, dass sie einen std::function Wrapper als Paramter hat und dann std::bind verwenden um einen Methodenzeiger in einen Wrapper zu packen. Dann geht es sowohl mit Methoden als auch Funktionen:

class OtherClass
{
public:
 void setFunction(const std::function<void()> &function)
 {
   this->function = function;
 }

 void callFunction()
 {
   if (function != NULL)
      function();
 }
private:
  std::function<void()> function;
};


class MyClass
{
public:
 MyClass()
  {
  }

 void doSomething()
 {
   cout << "Do something in method" << endl;
 }
private:
};

void doSomething()
{
  cout << "Do something in function" << endl;
}


int main()
{ 
  OtherClass otherClass;
  MyClass myClass;

  otherClass.setFunction(doSomething);
  otherClass.callFunction();

  std::function<void(void)> f = std::bind(&MyClass::doSomething, myClass);
  otherClass.setFunction(f);
  otherClass.callFunction();
 
}

Ausgabe:

Do something in function
Do something in method

Nur wegen Kommentaren wie "wieso geht sowas in C++ nicht" :slight_smile: