Nullstream Metaprogramming
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:
- Ich habe keine Ahnung, warum der Nullstream zwei Methoden hat.
operator<<(T(*)(T))
ist überflüssig, da auch funktionspointer intemplate <class T>
untergebracht werden können. Demzufolge entfernt dieser PR diese Methode -
template<class T>
ist einfach zu allgemein. Offensichtlich kompiliert das mit jeglichen Objekten. Nachdem mit-verbose
es nur noch mitCGA_Stream::operator<<
kompiliert, schränkt dieser PR dies auch für den Nullstream ein.
Dies hat zwei (vielleicht unerwartete) Effekte:
- 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 Methodeoperator<<(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 mitenable_if_t
und weiterer Metaprogrammierung sicherstellen, dassoperator<<(char*)
auf dem Nullstream immer angeboten wird, aber man muss sich darüber Gedanken machen, wie manambigous overload
verhindert. Durch die Verwendung von SFINAE auf der neuen MethodeshiftInternal
ist dies grundsätzlich möglich, man benötigt dafür nur einen passenden overload von shiftInternal fürchar*
. - Es gibt weiterhin Fälle, in denen
make all
kompiliert abermake 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:
- 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 MethodeO_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 MethodeshiftInternal
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 inambigous overload
zu geraten. - 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 wegenp = nullptr
UB wäre), sondern nur die Gültigkeit überprüft. - Der Kommaoperator (
*p << v, void()
) sorgt dafür, dass diese Methode den definierten Rückgabetypvoid
hat, egal was der Typausdruck*p << v
als Rückgabetype hätte (meistensO_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?