Überladung für Zeichenkettenliterale in C++

von Hubert Schmid vom 2012-11-25

Kann eine Funktion in C++ unterscheiden, ob sie mit einem Zeichenkettenliteral oder einer dynamisch zusammengebauten Zeichenkette aufgerufen wird? Die kurze Antwort ist: Nein. Denn Literale sind Ausdrücke mit einem Typ, der nicht vom Typ anderer Ausdrücke unterscheidbar ist. Dazu gleich mehr.

Hinter der Frage steckt ein konkreter Anwendungsfall: Bei dem Zeichenkettenparameter handelt es sich um eine Formatbeschreibung (vergleichbar zu printf), deren Verarbeitung relativ teuer ist. Zur Optimierung sollen die Analyse-Ergebnisse zwischengespeichert und wiederverwendet werden, und ein Kriterium für die Auswahl geeigneter Kandidaten sind Zeichenkettenliterale.

Im Gegemsatz zur allgemeinen Frage ist der konkrete Anwendungsfall lösbar. Denn die Unterscheidung muss nicht exakt sein und dient nur als Heuristik.

Typ der Zeichenkettenliterale

Der erste Lösungsweg überlädt die Funktion einfach für den Typ von Zeichenkettenliteralen. Nur: Welchen Typ haben Zeichenkettenliterale überhaupt? Im Zweifelsfall stellt man diese Frage einfach dem Übersetzer, denn der sollte es schließlich wissen:

template <typename Type> void deleted(Type&&) = delete; int main() { deleted("Hello World!"); // error: call to deleted function 'deleted' // note: candidate function [with Type = char const (&)[13]] // has been explicitly deleted }

Der Typ des Literals und Ausdrucks "Hello World!" ist also char const (&)[13] (gleichbedeutend mit const char (&)[13]), und hängt offensichlicht von der Länge der Zeichenkette ab. In der Praxis ist das ein hinreichendes Merkmal für die Überladung, und kann wie folgt eingesetzt werden:

void foobar(std::string const& format_string, bool cache = false); template <std::size_t N> void foobar(char (&format_string)[N], bool cache = false) { foobar(std::string{format_string}, cache); } template <std::size_t N> void foobar(char const (&format_string)[N], bool cache = true) { foobar(std::string{format_string}, cache); }

Der Ausdruck foobar("Hello World!") ruft wie gewünscht die dritte Funktion auf, wohingegen in den meisten anderen Fällen eine der beiden anderen Funktionen gewählt wird:

auto a = "Hello World!"; foobar(a); // calls first overload char const* b = "Hello World!"; foobar(b); // calls first overload (same as above) char c[] = "Hello World!"; foobar(c); // calls second overload std::string d{"Hello World!"}; foobar(d); // calls first overload foobar(d.c_str()); // calls first overload

Die wichtigste Ausnahme ist dem dritten Fall am ähnlichsten: char const s[] = "Hello World!";. Der Ausdruck foobar(s) führt in diesem Fall die dritte Funktion aus, obwohl kein Zeichenkettenliteral als Argument verwendet wurde. Das spielt für diesen Anwendungsfall aber keine Rolle, da eine solche Variable fast genauso statisch wie ein Zeichenkettenliteral ist.

Benutzerdefinierte Zeichenkettenliterale

Eine andere Lösung ergibt sich durch die Verwendung benutzerdefinierter Literale. Im Gegensatz zum ersten Lösungsweg ist die Unterscheidung an der Aufrufstelle expliziter und transparenter – mit den entsprechenden Vor- und Nachteilen. Das sieht dann wie folgt aus:

class string_literal; string_literal operator "" _s(char const* data, std::size_t size); void foobar(const std::string& format_string, bool cache = false); void foobar(string_literal&& format_string, bool cache = true) { foobar(static_cast<std::string>(format_string), cache); } int main() { foobar("Hello World!"); // calls first overload foobar("Hello World!"_s); // calls second overload }

Die Idee ist einfach: Die konstanten und für die Optimierung zu berücksichtigenden Zeichenkettenliterale werden durch ein spezielles Suffix gekennzeichnet. Diese Literale verwenden einen unveränderlichen Typ, der die Zeichenkette kapselt. Um den Missbrauch einzuschränken sollte man dessen Konstruktor noch private und die Literal-Funktion als einzigen friend deklarieren.

Welcher der beiden Ansätze besser ist hängt vom konkreten Szenario ab. Der erste Weg bietet den Vorteil, dass er mit C++98 funktioniert und keine Anpassung der Aufrufstellen erfordert. Der zweite Weg ist expliziter und transparenter – lohnt sich aber nur, wenn benutzerdefinierte Literale im Projekt bereits etabliert sind und in der Breite eingesetzt werden. Die beide Wege haben also zumindest so viele Unterschiede, dass die Abwägung in den meisten Fällen einfach sein sollte.