Liebe Leserinnen, liebe Leser,
in dem heutigen Beitrag teile ich eine Besonderheit über die Verbindung von selbstgeschriebenen Klassen mit vorhandenen Bibliotheken. Nehmen wir an, dass eine Bibliothek eine Funktion hat, die Rückruffunktionen akzeptiert, und es gibt auch eine Klasse, deren Klassenfunktionen von der Rückruffunktion benutzt werden müssen:
C++
1
2
3
4
5
// this function is part of OpenCV:
void cv::setMouseCallback(const String& winname, MouseCallback onMouse, void* userdata = 0);
// which accepts a callback with the following signiture:
typedef void(* cv::MouseCallback) (int event, int x, int y, int flags, void *userdata);
Nehmen wir auch an, dass die Nutzer unserer Klasse keine eigene Rückruffunktion für die Klasse schreiben, weil diese Aufgabe unsere Verantwortung ist, und wir sollten die Klasse von Anfang an so nutzbar machen, wie es möglich ist. Dieser Gedankengang scheint vernünftig zu sein, deswegen entscheiden wir uns, eine eigene Rückruffunktion anzubieten. Aber, wie soll man bei der Implementierung fortsetzen? Ein möglicher Ansatz wäre die Veröffentlichung aller erforderlichen Klassenfunktionen. Das würde den unmittelbaren Zugriff auf alle Sachen ermöglichen, ohne Zugriffunktionen zu haben:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ImgPaint {
public:
// ...
cv::Mat mMainImg;
private:
// ...
};
void paintCallback(int event, int x, int y, int flags, void* userdata)
{
ImgPaint& paint = *(ImgPaint* )userdata;
cv::Mat temp;
cv::Mat& mainImg = paint.mMainImg;
mainImg.copyTo(temp);
if (event == cv::EVENT_LBUTTONDOWN) {
//...
}
}
Dieser Code würde für nicht ganz Anfänger auf Anhieb die Alarm schlagen, weil die Exposition der Variablen am meistens als schlechte Praktik angesehen ist. Eine Behebung könnte das Verstecken von mMainImg und das Schreiben einer Zugriffunktion sein. Falls die Rückruffunktion mehrere Klassenfunktionen braucht, könnte man die gleiche Lösung verwenden:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ImgPaint {
public:
// ...
cv::Mat& getMainImg();
cv::Point getPt1() const;
cv::Point getPt2() const;
// ...
private:
cv::Mat mMainImg;
cv::Point2i mPt1;
cv::Point2i mPt2;
// ...
};
Aber, ist das wirklich eine gute Lösung? Natürlich, jetzt gibt es eine zusätzliche Ebene zwischen der Variable und Rückruffunktion, aber gibt es wirklich einen Unterschied hier? Wenn man es unter die Lupe stellt, erkennt man, dass diese Lösung zurück zum Anfang führt. Alles ist veröffentlicht, und die eingestellte zusätzliche Schicht hat im großen Ganzen gar nichts geändert. Wir erkennen das auch, dass alle diese Änderungen nur wegen einer Funktion gemacht wurde, deswegen fühlen wir uns schlechter.
Eine selbsterklärende Weise, durch die diese Probleme behoben werden können, ist der Umzug der Rückruffunktion in die Klasse statt der Freilegung der Variablen. Das versteckt die Variablen wieder, und der zusätzliche Vorteil ist, dass Sachen durch den void-Zeiger nicht mehr weitergegeben werden brauchen (und auf diese Weise können unangenehme Konvertierungen vermieden werden):
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ImgPaint {
public:
// ...
void paintCallback(int event, int x, int y, int flags, void* userdata);
private:
cv::Mat mMainImg;
// ...
};
void ImgPaint::paintCallback(int event, int x, int y, int flags, void* userdata)
{
cv::Mat temp;
mainImg.copyTo(temp);
if (event == cv::EVENT_LBUTTONDOWN) {
//...
}
}
Alles scheint in Ordnung zu sein, aber der Compiler stimmt wie vielmals früher nicht zu:
Make
1
2
3
4
5
6
7
8
9
10
11
./main.cpp: In function ‘int main()’:
./main.cpp:38:63: error: invalid use of non-static member function ‘void ImgPaint::mouseCallback(int, int, int, int, void*)’
cv::setMouseCallback(appName, paint.mouseCallback, appName);
^
In file included from ./main.cpp:1:0:
./ImgPaint.hpp:12:10: note: declared here
void mouseCallback(int event, int x, int y, int flags, void* userdata);
^~~~~~~~~~~~~
make[2]: *** [CMakeFiles/tutprog.dir/build.make:63: CMakeFiles/tutprog.dir/main.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:68: CMakeFiles/tutprog.dir/all] Error 2
make: *** [Makefile:84: all] Error 2
Hmm. Es würde so erscheinen, dass die Nutzung von nicht statischen Klassenfunktionen wegen des impliziten ersten Parameter (der auf this zeigt, und die Funktionssignatur infolgedessen ändert) auf diese Weise nicht erlaubt ist. Natürlich, man könnte den impliziten Parameter durch eine kleine Java-Weisheit entfernen, indem die Rückruffunktion und alle Variablen static deklariert sind. Nach ein bisschen Überlegung begegnet man ein paar Probleme:
- Obwohl static Klassenvariablen als privat deklariert werden können, müssen sie dennoch in einer cpp-Datei definiert werden. Das würde die Variablen für alle mal wieder sichtbar machen, obwohl auf sie weiterhin nur Klassenfunktionen zugreifen könnten. Das verhindert teilweise unserer Versuch, die Teile der Klasse zu kapseln, und das ist genung, diese Lösung abzugeben.
- Statische Klassenfunktionen können ohne die Änderung der Header-Datei nicht überladen werden (das ist wegen der strengen Anforderungen ein nicht so großes Problem für Rückruffunktionen).
Glücklicherweise bietet C++ für ähnliche Fällen einen Mechanismus an, der die meiste unsere Probleme einfach und elegant löst.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// header file
void paintCallback(int event, int x, int y, int flags, void* userdata);
class ImgPaint {
public:
// ...
friend void paintCallback(int event, int x, int y, int flags, void* userdata);
private:
cv::Mat mMainImg;
// ...
};
// source file
void paintCallback(int event, int x, int y, int flags, void* userdata)
{
ImgPaint& paint = *(ImgPaint* )userdata;
cv::Mat temp;
cv::Mat& mainImg = paint.mMainImg;
mainImg.copyTo(temp);
if (event == cv::EVENT_LBUTTONDOWN) {
//...
}
}
Durch die einfache Vordeklaration der Rückruffunktion und durch die Markierung dieser Funktion als friend bekommen wir einen Zugriff auf alles, das die Klasse enthält, ohne etwas auf irgendeine Weise freizulegen. Leider soll man Sachen durch den void-Zeiger von da an wieder übergegen. Das soll jetzt reichen.
Die Lektion des Tages ist:
Die beste Weise für die Versorgung der Rückruffunktionen ist die Deklaration der Rückruffunktion als friend der Klasse.
Wie immer, vielen Dank fürs Lesen.