[solved]is_printable - Probleme mit template specialization



  • Hallo Community!

    Ich schreibe derzeit eine Menüklasse für die Konsole, damit man mit 1 - 3 Zeilen hübsche Menüs samt Titel und Eingabeaufforderung auf den Bildschirm zaubern kann. Beim Konstruktor des Menüs kann man dann beliebig viele Werte übergeben, die dann als Menü Einträge dienen sollen. Zur Compile-Zeit soll jedoch erstmal überprüft werden, ob die übergebenen Werte überhaupt mit std::cout auf den Bildschirm ausgegeben werden können. Dazu schrieb ich mir die Klasse is_printable, die ich dann nachher in Kombinierung mit std::enable_if verwenden kann.

    Meine is_printable-Implementation sieht wie folgt aus:

    namespace is_printable_impl{
        typedef char yes;
        typedef char no[2];
    
        template<typename Type>
        const no& operator<< (const std::ostream&, const Type&);
    
        const yes& test(const std::ostream&);
        const no& test(const no&);
    
        template<typename Type>
        class is_printable{
            static const Type& type;
    
        public:
            static const bool value = sizeof(test(std::cout << type)) == sizeof(yes);
        };
    
        template<typename Type>
        class is_printable<Type[]>{
        public:
            static const bool value = true;
        };
    
        template<typename Type>
        class is_printable<Type*>{
        public:
            static const bool value = true;
        };
    }
    
    template<typename Type>
    class is_printable : public is_printable_impl::is_printable<Type> {};
    

    Bevor ich nun meine is_printable-Klasse in der Menü-Klasse verwende, wollte ich sie natürlich testen. Nur ein Test schlug fehl, wobei ich mit einem Haufen Fehlermeldungen endete. Folgender Code:

    int main(){
        std::cout << is_printable<int[]>::value << '\n' // kompiliert
                  << is_printable<int[5]>::value;       // kompiliert nicht
        return 0;
    }
    

    produziert bei mir folgende Fehlermeldungen:

    [Ich erspare euch das Scrollen in diesem Thread]

    Und obwohl ich extra eine Template-Specialization für Arrays angelegt habe (siehe erster Code - Zeile 20 [die mit den Pointern funktioniert einwandfrei]), bekomme ich diese Fehler. Wie kann ich das jetzt ändern?



  • Ich Dödel.
    Da hab ich mal wieder zu schnell gepostet.

    Hier die Lösung:

    namespace is_printable_impl{
        typedef char yes;
        typedef char no[2];
    
        template<typename Type>
        const no& operator<< (const std::ostream&, const Type&);
    
        const yes& test(const std::ostream&);
        const no& test(const no&);
    
        template<typename Type>
        class is_printable{
            static const Type& type;
    
        public:
            static const bool value = sizeof(test(std::cout << type)) == sizeof(yes);
        };
    
        template<typename Type>
        class is_printable<Type[]>{
        public:
            static const bool value = true;
        };
    
        template<typename Type, std::size_t N>
        class is_printable<Type[N]>{
        public:
            static const bool value = true;
        };
    
        template<typename Type>
        class is_printable<Type*>{
        public:
            static const bool value = true;
        };
    }
    
    template<typename Type>
    class is_printable : public is_printable_impl::is_printable<Type> {};
    

    Tut mir Leid für den unnötigen Thread.


  • Mod

    template<typename Type>
        const no& operator<< (const std::ostream&, const Type&);
    

    Das ist schon ziemlich speziell, wenn du Pech hast, kollidiert das mit einem existierenden Operator (z.B. einer, der seiner Parameter nur indirekt per enable_if einschränkt).
    Für besser halte ich so etwas wie

    struct sink { template <typename T> sink(T); };
        const no& operator<< (const std::ostream&, sink);
    


  • camper schrieb:

    template<typename Type>
        const no& operator<< (const std::ostream&, const Type&);
    

    Das ist schon ziemlich speziell, wenn du Pech hast, kollidiert das mit einem existierenden Operator (z.B. einer, der seiner Parameter nur indirekt per enable_if einschränkt).
    Für besser halte ich so etwas wie

    struct sink { template <typename T> sink(T); };
        const no& operator<< (const std::ostream&, sink);
    

    Soso.
    Dann war der Thread wohl doch nicht ganz so unnötig - Dankeschön! 🙂



  • 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.



  • 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.

    Uuh nice!
    Mir war std::declval noch nicht bekannt.
    Dankeschön.


  • 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