Array übergeben zu Konstruktor


  • Mod

    @Belli sagte in Array übergeben zu Konstruktor:

    Da ich mit Templates normalerweise nichts mache, habe ich da mal ne ganz konkrete Frage:

    Ich habe Objekte vom Typ T in einem Container, mit denen eine Funktion was machen soll.
    Damit die Funktion nicht wissen muss, welchen Typ der Container hat, übergebe ich ihr lediglich Iteratoren:

    template<typename iterator>
    void func(iterator begin, iterator end)
    {
      // ...
    }
    

    so weit, so gut. Kann (sollte ich überhaupt) ich meine Funktion so gestalten (Spezialisierung?), dass sie nur mit Containern arbeitet, die auch meinen Typ T beinhalten und wie würde das aussehen?

    Oder macht man das gar nicht, weil die Funktion sich nicht übersetzen lässt, wenn eine Operation auf dem tatsächlich im Container übergebenen Typ T nicht durchgeführt werden kann?

    Das kommt drauf an. Ist halt schwierig, konkrete Designfragen zu einer Funktion func mit Typ Tzu beantworten 🙂
    So wies es aussieht, spielt die Natur von T hier aber gar keine Rolle, denn es kommt in deinem Beispiel ja gar nicht vor. Oder soll das eine Memberfunktion von T sein?

    Wenn das eine freie Funktion ist, dann schaut es mir so aus, als solle sie völlig unabhängig von T sein.

    Wenn das eine Memberfunktion ist, kommt es ein bisschen mehr auf Semantik an, und wir können die Frage nicht klar beantworten. Mir fällt jetzt aber auch kein gutes Beispiel ein, wo so etwas unbedingt ein Iterator<T> sein müsste. Aber es gibt bestimmt irgendwas. Vielleicht irgendwelche Algebraoperationen, wo man "Mischung" von bestimmten Objekttypen vermeiden will? Wobei Algebra aber auch ein gutes Beispiel für die Vorteile von generischen Funktionen sind, denn vieles ist ja immer die gleiche Operation, aber mit anderen Objekttypen.



  • Ich hab mich wohl missverständlich ausgedrückt ...
    Ich habe kein konkretes Beispiel/Problem, es war eine theoretische Frage:
    Mein T kommt doch vor, oder bin ich völlig auf dem Holzweg ...
    Wenn ich innerhalb der Funktion sowas wie:

    while(begin != end)
    {
       cout << *begin;
       // ...
    }
    

    mache, dann ist doch *begin vom Typ T?!
    Und vielleicht macht meine Funktion irgendwas, was nur für T Sinn ergibt, und will lediglich dafür offen sein, dass die Objekte in einem beliebigen Container gehalten werden.
    Würde man dann sicherzustellen versuchen, dass keine Iteratoren für Container mit Objekten, die nicht T sind, übergeben werden können?
    Sagen wir mal, die Funktion soll für alle Elemente die Quadratwurzel ermitteln ... dann geht das nicht, wenn die Objekte vom Typ string sind.
    Überlässt man das dann sozusagen sich selbst weil die Funktion mit T = string ja nicht übersetzt werden kann?
    Oder könnte es auch Konstellationen geben, die übersetzt werden können, aber ein unerwünschtes Ergebnis haben ...
    Vielleicht ... addiere zwei aufeinanderfolgende Elemente: *begin + *(begin + 1), *(begin + 2) + *(begin + 3), ...
    Das würde zB für int, double funktionieren, aber auch für string und vielleicht für ein paar selbstdefinierten Typen.

    Wenn das nun für einige aber keinen Sinn ergäbe, würde man dann den Aufruf mit diesen Typen verhindern wollen, oder sich bei Definition dieses Templates nicht darum kümmern?

    Also sozusagen generisch bzgl. des Containertypen, aber festgelegt auf einen bestimmten Typ in dem Container ... oder ist das alles völliger Blödsinn?



  • @SeppJ sagte in Array übergeben zu Konstruktor:

    So sollte man das machen, und so ist das von mir gemeint. Oder halt meinetwegen mit Iteratoren, wenn man auf älteren Sprachstandards unterwegs ist. Diese beiden Möglichkeiten muss jeder wissen, dass das die Methode ist, wie man containerartige Datenmengen herumreicht. Ausnahmen gibt es nur, wenn man selber containerartige Strukturen schreibt, da ist klar, dass die natürlich auch sich selber benutzen können. Und std::string, mathematische Vektoren und ähnliches, weil die eine spezielle Semantik haben, die über "Folge von Zeichen/Zahlen" hinaus geht.

    Ich würde sagen es gibt viel mehr Dinge die eine spezielle Semantik haben - bzw. wo es einfach Sinn macht es nicht generisch zu machen.

    Und ich finde in grösseren Anwendungen gibt es auch oft gute Gründe die Container weiter einzuschränken. z.B. Compilezeit und Template-Bloat.

    span und string_view sind da IMO oft sehr gut geeignet.

    Davon abgesehen: generischen, wiederverwendbaren Code zu schreiben ist sowohl ziemlich schwer als auch ziemlich aufwendig. Damit ein paar Templates wo drüberzustreuen um konkrete Containertypen zu vermeiden ist es - ausser in sehr einfachen Fällen - lange nicht getan. Mein Rat wäre eher nicht zu viel Zeit damit zu verschwenden zu versuchen Dinge wiederverwendbar zu machen - es sei denn man hat ein solides Verständnis der Problemdomäne, z.B. weil man schon 1-2x eine wenig/nicht-generische Lösung implementiert hat.



  • @Belli sagte in Array übergeben zu Konstruktor:

    Überlässt man das dann sozusagen sich selbst weil die Funktion mit T = string ja nicht übersetzt werden kann?

    Das wäre der alte Weg und das in Kombination mit sehr kryptischen Fehlermeldungen des Compilers, wobei die Compiler in dieser Hinsicht teilweise besser geworden sind. Leider wird noch immer viel zu viel Müll emittiert und man muss zuerst die eigentliche Fehlermeldung suchen. Ab C++20 ist der neue Weg Konzepte (Concept), so dass man bestimmte Eigenschaften des übergebenen Typen T einfordert z.B. das sqrt(T) definiert ist. Dadurch werden die Fehlermeldungen ebenfalls klarer.



  • @hustbaer sagte in Array übergeben zu Konstruktor:

    Davon abgesehen: generischen, wiederverwendbaren Code zu schreiben ist sowohl ziemlich schwer als auch ziemlich aufwendig. Damit ein paar Templates wo drüberzustreuen um konkrete Containertypen zu vermeiden ist es - ausser in sehr einfachen Fällen - lange nicht getan. Mein Rat wäre eher nicht zu viel Zeit damit zu verschwenden zu versuchen Dinge wiederverwendbar zu machen - es sei denn man hat ein solides Verständnis der Problemdomäne, z.B. weil man schon 1-2x eine wenig/nicht-generische Lösung implementiert hat.

    Genau dies!

    Auch das Duck-Typing in Python, das @SeppJ angesprochen hat, funktioniert nur zu einem gewissen Grad. Ich habe zum Beispiel an dieversen Stellen die Voraussetzung, dass mein "generisches Iterable" in Wirklichkeit ein numpy.array (oder eine pandas.Series) ist. Auch die Vorstellung, dass Code auf einmal für std::string sinnvoll ist, weil man über Strings iterieren kann, ist in der Regel nicht mehr als eine Wunschvorstellung, die außer für absolut generische Algorithmen nie sinnvoll funktioniert.

    Natürlich muss man sich nicht künstlich einschränken. Und ja, es kann auch hilfreich sein, nur "am Ende" den konkreten Typ zu fordern und "in der Mitte" alles ganz generisch zu halten, damit man irgendwann mal theoretisch den Typ am Ende austauschen kann. Aber die Frage ist doch immer: a) ist es realistisch, dass das überhaupt jemals passiert und b) was ist der Aufwand dafür? Allein schon, dass man beim Entwickeln von generischem Code keine Syntaxvervollständigung hat, kostet viel Entwicklerzeit. In 90% aller Fälle werfe ich den Code nach dem Projektabschluss weg. Bzw. stelle bei einem nachfolgenden Projekt fest, wo es Überschneidungen gibt. Dann kann ich gucken, was davon sich generisch umzusetzen lohnt. Wenn ich gleich immer alles generisch mache, dauert alles viel zu lange.



  • @wob sagte in Array übergeben zu Konstruktor:

    Allein schon, dass man beim Entwickeln von generischem Code keine Syntaxvervollständigung hat, kostet viel Entwicklerzeit.

    Meiner Erfahrung nach ist die Syntaxvervollständigung eine der größten Produktivitätsbremsen überhaupt. Immer wieder aktiviert sich die Autovervollständigung, wenn man sie nicht braucht und dann ist schnell etwas ergänzt was man nicht braucht und muss es wieder umständlich löschen.


  • Mod

    @Belli sagte in Array übergeben zu Konstruktor:

    Mein T kommt doch vor, oder bin ich völlig auf dem Holzweg ...
    Wenn ich innerhalb der Funktion sowas wie:

    while(begin != end)
    {
       cout << *begin;
       // ...
    }
    

    mache, dann ist doch *begin vom Typ T?!

    Ja, aber ist es hier wichtig, dass es ein T ist? Da müsste ja schon eher so etwas stehen wie

    T copy = *begin;
    

    damit der Typ T wirklich im Code vor kommt. Bei so einer Zeile ist es dann eine Frage der Semantik, ob das für die Funktionalität ein T sein muss, oder ob das eher ein von *begin automatisch abgeleiteter Typ sein soll.



  • @Belli
    Da biste schnell im Bereich SFINAE, Überladungen, Spezialisierungen und std::enable_if bzw. std::conditional.
    Grundsätzlich lassen sich templates nicht erzeugen, wenn für den Template Parameter bestimmte Operationen nicht existieren (in deinem Beispiel die Wurzel aus nem string). Dann wirft der Compiler eine Fehlermeldung.
    Man kann Templates spezialisieren, dann wird für den "Normalfall" das allgemeine Funktionstemplate benutzt und für die Spezialisierung die, ähm, Spezialisierung. Weil das zwei unterschiedliche Funktionen sind können die sich auch unterschiedlich verhalten.
    Sinnfreies Beispiel:

    template<typename T>
    std::size_t size_of( T const& val )
    {
       return sizeof( val );
    }
    
    template<>
    std::size_t size_of( std::string const& val )
    {
       return val.length();
    }
    

    Mit std::enable_if oder std::conditional kannst du Funktionstemplates gezielt nur für solche Typen erlauben, für die das Prädikat zutrifft:

    template<typename T>
    typename std::enable_if<std::is_arithmetic<T>::value,T>::type double_up( T value )
    {
    	return value + value;
    }
    

    funktioniert für alle arithmetischen Datentypen, aber nicht für std::string, obwohl std::string den +-Operator unterstützt. Da ist man jetzt aber schon im Template Meta Programming, das brauche ich zumindest so gut wie nie. Aber ne hübsche Spielerei um zu gucken, was man wie weit verstanden hat.

    Und wenn das dann vom Typ abhängen soll, den der iterator referenziert, kannste den über iterator_traits auch noch bestimmen:

    template<typename Iterator>
    void func( Iterator beg, Iterator end )
    {
       using value_type = typename std::iterator_traits<Iterator>::value_type;
       // hier weitere Überprüfung, zB. per type_traits und static_assert
    }
    


  • @DocShoe sagte in Array übergeben zu Konstruktor:

    @Belli
    Da biste schnell im Bereich SFINAE, Überladungen, Spezialisierungen und std::enable_if bzw. std::conditional.

    Das ist mittlerweile schon wieder old school.

    Sinnfreies Beispiel:

    template<typename T>
    std::size_t size_of( T const& val )
    {
       return sizeof( val );
    }
    
    template<>
    std::size_t size_of( std::string const& val )
    {
       return val.length();
    }
    

    Mit std::enable_if oder std::conditional kannst du Funktionstemplates gezielt nur für solche Typen erlauben, für die das Prädikat zutrifft:

    template<typename T>
    typename std::enable_if<std::is_arithmetic<T>::value,T>::type double_up( T value )
    {
    	return value + value;
    }
    

    In C++20 schreibt das so

    template <typename T>
    T double_up(const T& value) requires std::is_arithmetic<T>::value {
        return value + value;
    };
    


  • Jo, das ist wohl so. Da ich mich mit einem Compiler rumärgern muss, der gerade mal C+11 unterstützt, hänge ich da hinterher.

    Geht da nicht sogar iwas mit decltype und auto?

    #include <type_traits>
    
    auto double_up( auto const& value) requires typename std::is_arithmetic<decltype(value)>::value
    {
        return value + value;
    };
    
    int main()
    {
        double_up( 2.0 );
    }
    

    Krieg´ die Syntax nicht hin, das übersetzt so nicht.



  • Hier sind zwei Varianten die funktionieren.

    #include <type_traits>
      
    template <typename T>
    concept Addable = requires (T a, T b) {a + b;};
    
    auto double_up (const auto& value) requires Addable<decltype(value)> {
        return value + value;
    }
    
    // alternativ
    auto double_up (const Addable auto& value) {
        return value + value;
    }
    
    int main() {
        double_up (2.0);
    }