Skip to content

Nullstream Metaprogramming

Tobias Heineken requested to merge yn87ivuv/mpstubs:PR_null2 into master

Im Rahmen von MPStubs bin ich inzwischen oft genug über das gleiche Problem gestolpert:

DBG_VERBOSE << a; kompiliert immer, ganz egal ob es einen operator<<(decltype(a) auf CGA_Stream gibt. Erst wenn durch make all-verbose DBG_VERBOSE von Nullstream auf CGA_Stream umgeschaltet wird, gibt das dann haufenweise Compilerfehlermeldungen.

Da ich gerne schon beim schreiben den Codes immer wieder den Compiler prüfen lassen will, ob das sinnvoll erscheint, und dafür nicht immer gleich mit -verbose bauen möchte (weil verbose in meiner Implementierung doch ziemlich verbose ist und ich es wirklich nur für die ganz seltenen Fälle wirklich brauche), habe ich mir mal den Nullstream genauer angeschaut.

Es stellen sich zwei Dinge heraus:

  1. Ich habe keine Ahnung, warum der Nullstream zwei Methoden hat. operator<<(T(*)(T)) ist überflüssig, da auch funktionspointer in template <class T> untergebracht werden können. Demzufolge entfernt dieser PR diese Methode
  2. template<class T> ist einfach zu allgemein. Offensichtlich kompiliert das mit jeglichen Objekten. Nachdem mit-verbose es nur noch mit CGA_Stream::operator<< kompiliert, schränkt dieser PR dies auch für den Nullstream ein.

Dies hat zwei (vielleicht unerwartete) Effekte:

  1. Die Aufgabe, die Vererbungshirarchie in CGA_Stream um O_Stream zu erweitern widerspricht der Angabenstellung: Um die in main.cc bereits existiertenden Ausgabe kompilieren zu können, muss CGA_Stream eine Methode operator<<(char*) anbieten. Dieser PR löst das indem die Vererbung bereits implementiert ist und ausschließlich die fehldene Vererbung noch eingetragen werden muss. Eventuell lässt sich mit enable_if_t und weiterer Metaprogrammierung sicherstellen, dass operator<<(char*) auf dem Nullstream immer angeboten wird, aber man muss sich darüber Gedanken machen, wie man ambigous overload verhindert. Durch die Verwendung von SFINAE auf der neuen Methode shiftInternal ist dies grundsätzlich möglich, man benötigt dafür nur einen passenden overload von shiftInternal für char*.
  2. Es gibt weiterhin Fälle, in denen make all kompiliert aber make all-verbose nicht. Dies sind vorrangig Linker-fehler, wie auch im im PR vorliegenden Beispiel der Fall. Sollten Kompilerfehler nur auf einer der beiden Varianten auftreten, gerne mit Codebeispiel zurück an mich :)

Zur Implementierung:

  1. Um erweiterbarkeit zu erleichtern ist der Compilefehler über SFINAE implementiert. Das bedeutet, falls für einen Typ T die implementierte Methode shiftInternal ungültig wäre (weil keine Methode O_Stream::operator<<(T) sind fand), ist es noch kein Compilerfehler, sondern diese Methode wird nur aus der Menge an betrachteten Funktionen für Overload-Resolution geworfen. Will man zusätzlich noch weitere Klassen auf den Nullstream shiften können, so kann man eine weitere Methode shiftInternal anlegen, die für diesen Typ passend ist. In diesem Fall kann man sogar (was für Nullstream nur mäßig nützlich erscheint) anderen Code ausführen. Dabei ist darauf zu achten, nicht in ambigous overload zu geraten.
  2. Der PR verwendet Expression-SFINAE nach https://en.cppreference.com/w/cpp/language/sfinae#Expression_SFINAE, in Verbindung mit trailing return type (auto function(parameter) -> return-type), um den Ausdruck *p << v auf Gültigkeit zu prüfen. Man achte darauf, das hier keine Auswertung des Ausdrucks passiert (der wegen p = nullptr UB wäre), sondern nur die Gültigkeit überprüft.
  3. Der Kommaoperator (*p << v, void()) sorgt dafür, dass diese Methode den definierten Rückgabetyp void hat, egal was der Typausdruck *p << v als Rückgabetype hätte (meistens O_Stream&, aber wer weiß...)

Liebe Grüße, und ein frohes neues Jahr, Tobias Heineken

PS: Nachdem PRs für die Vorlage gesperrt sind, habe ich statdessen dein Repo geforkt. Könntest du das auf die Vorlage weitergeben?

Merge request reports