[solved]is_printable - Probleme mit template specialization


  • Mod

    einself schrieb:

    Mit C++11 geht das auch so:

    template <typename T, typename=void>
    struct is_printable : std::false_type {};
    template <typename T>
    struct is_printable<T, typename std::enable_if<std::is_same<std::ostream&, decltype(std::declval<std::ostream&>() << std::declval<T>())>::value>::type> : std::true_type {};
    

    Da sind sogar noch etwas mehr Checks drin wie bei dir.

    Etwas umständlich allerdings.

    template <typename T>
    struct is_printable<T> : std::is_same<std::ostream&, decltype(std::declval<std::ostream&>() << std::declval<T>())> {};
    

    tut es auch.



  • Hätte nicht gedacht, dass man meine 40 Zeilen Code von vorhin auf 2 Zeilen reduzieren kann (und obendrein besser ist).
    Templates sind echt geile Dinger.



  • camper schrieb:

    template <typename T>
    struct is_printable<T> : std::is_same<std::ostream&, decltype(std::declval<std::ostream&>() << std::declval<T>())> {};
    

    tut es auch.

    Was ist mit is_printable<void>::value ?



  • Mist entfernt


  • Mod

    einself schrieb:

    Was ist mit is_printable<void>::value ?

    Da war ich wohl etwas zu aggressiv...

    auf enable_if und is_same kann man jedenfalls verzichten

    template <typename T, typename = std::ostream&>
    struct is_printable : std::false_type {};
    template <typename T>
    struct is_printable<T, decltype(std::declval<std::ostream&>() << std::declval<T>())> : std::true_type {};
    


  • Ich hab da doch noch ein Verständnisproblem.
    Wie ist das mit typename = XYZ zu verstehen?

    So wie ich das verstehe:
    Wenn der Ausdruck decltype(std::declval<std::ostream&>() << std::declval<BadType>()) in der Template-Spezialisierung nicht kompilierbar ist wird die andere is_printable Klasse genommen. Aber warum dieses typename = std::ostream& ?

    Meine Vermutung: typename = std::ostream& "sagt" dem Compiler, dass der eigentliche Rückgabewert halt ein std::ostream& hätte sein sollen?



  • gnihihihihi schrieb:

    Wie ist das mit typename = XYZ zu verstehen?

    Das ist ein unbenannter Template-Parameter, wie int f(int) einen unbenannten Funktionsparameter hat. Und er hat einen Default-Wert, wie in int f(int=0) .

    template <typename T, typename=void> struct is_printable {};
    template <typename T> struct is_printable<T, void> {};
    

    Hier sind Default-Werte verbunden mit Spezialisierung, da passiert das:

    is_printable<X>::value
    // wird zu
    is_printable<X, void>::value
    // d.h. die Spezialisierung wird genommen
    

    Wenn die Spezialisierung nicht auf einen festen Typ geht, sondern auf

    typename std::enable_if<std::is_same<std::ostream&, decltype(std::declval<std::ostream&>() << std::declval<T>())>::value>::type
    

    dann muss zuerst aufgelöst werden, was für ein Typ das genau ist.

    Jetzt kommt SFINAE ins Spiel. Wenn std::declval<std::ostream&>() << std::declval<T>() nicht kompiliert, wird abgebrochen. Bei std::enable_if<false>::type wird ebenfalls abgebrochen. Abbrechen bedeutet, dass die Spezialisierung nicht genommen wird.

    Und std::enable_if<true>::type hat genau den Typ void , dann passiert das wie im einfachen Beispiel.

    Dieses void hat bei mir nur den Zweck, SFINAE zu erlauben. Camper zieht daraus aber einen grösseren Nutzen.

    template <typename T, typename=std::ostream&> struct is_printable {};
    template <typename T> struct is_printable<T, ...> : std::true_type {};
    

    Die Spezialisierung wird genau dann genommen, wenn 1. ... kompilierbar ist (SFINAE) und 2. wenn es gleich std::ostream& ist. Aus dem gleichen Grund, weil is_printable<X> gleich is_printable<X, std::ostream&>::value ist, und die Spezialisierung bevorzugt wird; aber nur wenn sie auf std::ostream& greift.

    Verallgemeinert willst du "ist Ausdruck A für Typ T kompilierbar?" beantwortet bekommen. Dafür gibt es mehrere idiomatische Ansätze.

    • Der alte ist der mit den yes/no-Typen.
    • Die "neue Version" ist mit constexpr:
    template <typename T, typename=decltype(<<<Ausdruck A>>>)>
    constexpr bool is_XXX_(int) { return true; }
    template <typename> constexpr bool is_XXX_(...) { return false; }
    template <typename T> struct is_XXX : std::integral_constant<bool, is_XXX_<T>(0)> {};
    
    • Dann gibt es den enable_if-Hack mit dem unbenannten Template-Parameter,
    template <typename T, typename=void> struct is_XXX : std::true_type {};
    template <typename T> struct always_true : std::true_type;
    template <typename T> struct is_XXX<T, std::enable_if<always_true<<<Ausdruck A>>> >::value > : std::false_type {};
    

    Ich mag den constexpr -Ansatz am liebsten, meistens wird das nur intern gebraucht, dann kann auf die Helfer-Klasse verzichtet werden. Und wenn das dem Nutzer zur Verfügung steht, sollte das is_XXX nochmals gewrappt werden, damit der Nutzer nicht aus Versehen 2 Parameter an is_XXX übergeben kann.

    Auf jeweilige Fälle kann das Grundgerüst natürlich optimiert werden. Dieses always_true wird almost always nie benötigt (is_same z.B.). Camper hat das Optimieren in die Spitze getrieben. Ich mag das nicht so arg, weil typename=void sagt mir sofort "aha, das ist dieser Hack" und ich verstehe den Code augenblicklich. Bei der anderen Variante sehe ich das erst auf den zweiten Blick. TMP soll ja auch lesbar bleiben.


  • Mod

    gnihihihihi schrieb:

    Wie ist das mit typename = XYZ zu verstehen?

    Das sagt dem Compiler, dass is_printable eine 2. (Typ-)Templateparameter hat, und dieser einen Defaultwert hat, wenn er nicht explizit angegeben wird. Der Templateparameter hat keinen eigenen Namen bekommen.
    is_printable<T> ist somit in Wirklichkeit is_printable<T,std::ostream&>

    gnihihihihi schrieb:

    So wie ich das verstehe:
    Wenn der Ausdruck decltype(std::declval<std::ostream&>() << std::declval<BadType>()) in der Template-Spezialisierung nicht kompilierbar ist wird die andere is_printable Klasse genommen. Aber warum dieses typename = std::ostream& ?

    Kleiner Exkurs: wie bestimmt der Compiler welche Spezialisierung eines Klassentemplates zu verwenden ist?

    Betrachten wir irgendein Primärtemplate

    template <typename T, typename U> struct foo;
    

    Nehmen wir an, es gibt eine partielle Spezialisierung

    template <typename X, typename Y, typename Z> struct foo<type1, type2>;
    

    wobei type1 und type2 irgendwie von X,Y,Z abhängen.

    Nehmen wir an, wir haben für die die Templateparameter T und U irgendwelche Argumente A und B. Wenn nun foo<A,B> instantiiert wird, muss der Compiler ermitteln, ob das Primärtemplate zu verwenden ist oder aber die gegebene Spezialisierung. Um die Spezialisierung verwenden zu können, muss der Compiler aus dem gegeben Tupel <A,B> die zugehörigen Parameter X,Y,Z der Spezialisierung deduzieren. Die Deduktion ist erfolgreich, wenn aus sich aus ermittelten X,Y,Z durch die Regeln type1,type2 das Tupel <A,B> entsteht.
    Wenn das gelingt, ist die Spezialisierung zu verwenden (wenn mehrere Spezialisierungen existieren, die danach verwendet werden, müssen diese partiell geordnet sein und es wird die nach den Regel der partiellen Ordnung von Templatespezielaisierungen die am stärksten spezialisierte Variante verwendet, darauf gehe ich hier nicht weiter ein) sonst das Primärtemplate.

    Ich ändere mal die Paramternamen ein wenig

    template <typename T, typename U = std::ostream&>
    struct is_printable : std::false_type {};
    template <typename V>
    struct is_printable<V, decltype(std::declval<std::ostream&>() << std::declval<V>())> : std::true_type {};
    

    Wenn is_printable<A> instantiiert wird, ist das erst einmal is_printable<A,std::ostream&>.
    Um zu ermitteln, ob die Spezialisierung zu verwenden ist, muss für das gegebene Tupel <A,std::ostream&> versucht werden, V zu synthetisieren.
    Der decltype(...)-Typ stellt offenbar einen nicht deduzierbaren Kontext dar.
    Zum Glück ist das erste Element des Tuples (A) gegeignet, um V zu ermitteln, nähmliche (trivial) V=A.
    Um nun zu wissen, ob diese Deduktion korrekt ist, substituieren wir V mit A in der Argumentliste der partiellen Spezialsierung:
    <A, decltype(std::declvalstd::ostream&() << std::declval<A>())>
    Wenn diese Substitution erfolgreich ist und das Tuple <A,std::ostream&> ergibt, so wird die Spezialisierung verwendet.
    Das ist offenbar nur dann der Fall, wenn der Ausdruck std::declvalstd::ostream&() << std::declval<A>() wohlgeformt ist (also vor allem ein geeigneter Operator existiert), und als Ergebnis auch tatsächlich ein std::ostream& liefert.
    Das ist nun (nicht ganz zufällig...) genau das, was wir von A wissen wollen.

    (Die Deduktionlogik ist identisch mit der, die bei Templatefunktionen verwendet wird).



  • Sehr hübsch, danke. 👍
    Gleich mal in meine Loggerklasse per static_assert eingebaut, 1 Fehlermeldung anstatt über 50. 🙄



  • Danke für die Erklärungen!

    Dann hab ich ja nun doch noch einige Dinge mit diesem Thread dazugelernt 🙂

    Grüße.


  • Mod

    einself schrieb:

    Camper hat das Optimieren in die Spitze getrieben. Ich mag das nicht so arg, weil typename=void sagt mir sofort "aha, das ist dieser Hack" und ich verstehe den Code augenblicklich. Bei der anderen Variante sehe ich das erst auf den zweiten Blick. TMP soll ja auch lesbar bleiben.

    Da stimme ich zu. Meine "optimierte" Variante hat vor allem den Nachteil, dass die Bedingung über beide Templates verteilt wird, anstatt bei enable_if an einer Stelle zu stehen. Das ist unter dem Gesichtspunkt der Wartbarkeit ein Problem. Sobald die Bedingung komplexer wird und nicht mit einem einfachen is_same erschlagen werden kann, wird es ohne enable_if ohnehin komplizierter (oder man hat zusätzliche Codeduplikation).
    Kurz: Beide Varianten beruhen auf dem gleichen Prinzip, einselfs Variante ist aber besser wiederverwendbar und also als Idiom geeignet.


Anmelden zum Antworten