Array übergeben zu Konstruktor



  • @SeppJ sagte in Array übergeben zu Konstruktor:

    Code, der in C++ eine explizite Containervariante vorschreibt um eine Menge von Daten zu übergeben, ist einfach falsch. Das muss so auch gesagt werden und jedem bewusst sein.

    Das ist einfach nicht wahr.

    Der Code ist vielleicht nicht idiomatisch. Aber falsch noch lange nicht.

    Außerdem: man hat ja oftmals auch Code, der unterschiedliche Performacecharakteristika hat je nach Container bzw. bei dem für jeden Container ein anderer Algorithmus besser ist - d.h. ich muss eh wissen, ob ich da eine Liste oder einen Vector habe. (Gut, du wirst jetzt sagen, ich muss wissen, ob ich einen RadomAccessIterator oder einen ForwardIterator habe) Alles schön und gut. Aber mit einem konkreten Objekt arbeitet es sich nun einfach mal leichter. Und nicht jeder Code muss generisch sein, finde ich.

    Um so schlimmer (und ein bekanntes Antipattern!) ist in Python Code, der irgendwelche Typprüfungen macht. Jeder Best-Practices-Guide für Python sagt, möglichst niemals Konstrukte wie if isinstance(variable, list) zu nutzen.

    Und auch hier muss ich widersprechen. Ich habe solchen Code nämlich. Der geht eine Datenstruktur beim Serialisieren rekursiv durch - und der testet sehr wohl direkt auf "list" (und noch ein paar andere Typen, die alle gesondert behandelt werden). Und ich habe auch diverse Funktionen, die 1 Arg oder einen Container zulassen - auch da muss ich explizit checken - und zwar auf "not isinstance(obj, str)", weil Strings nämlich auch iterable sind (und ich eben den Fall 1 String oder mehrere Strings zulassen will)


  • Mod

    @wob sagte in Array übergeben zu Konstruktor:

    Der Code ist vielleicht nicht idiomatisch. Aber falsch noch lange nicht.

    Es ist halt falsch im Sinne von dass C mit cout auch falsches C++ ist. Es gibt keinen Grund es nicht richtig zu machen, das richtige hat nur Vorteile, daher ist es falsch, es nicht richtig zu machen.

    Gut, du wirst jetzt sagen, ich muss wissen, ob ich einen RadomAccessIterator oder einen ForwardIterator habe

    Ja? Ist das so schlimm? Du brauchst ja nix dafür zu tun. Du schreibst doch einfach nur Code als ob's ein Vector wäre und entweder kann das Ding operator[] oder eben nicht. Da freuen sich dann die deque- und string-Nutzer und für alle anderen bleibt alles gleich.

    @wob sagte in Array übergeben zu Konstruktor:

    Um so schlimmer (und ein bekanntes Antipattern!) ist in Python Code, der irgendwelche Typprüfungen macht. Jeder Best-Practices-Guide für Python sagt, möglichst niemals Konstrukte wie if isinstance(variable, list) zu nutzen.

    Und auch hier muss ich widersprechen. Ich habe solchen Code nämlich. Der geht eine Datenstruktur beim Serialisieren rekursiv durch - und der testet sehr wohl direkt auf "list" (und noch ein paar andere Typen, die alle gesondert behandelt werden). Und ich habe auch diverse Funktionen, die 1 Arg oder einen Container zulassen - auch da muss ich explizit checken - und zwar auf "not isinstance(obj, str)", weil Strings nämlich auch iterable sind (und ich eben den Fall 1 String oder mehrere Strings zulassen will)

    Der String ist eine wohl seltene Ausnahme, die ich auch abundzu benutze, aus genau dem gleichen Grund. Aber dir ist schon bewusst, dass das Gegenteil (zu prüfen, ob es mehrere Strings sind, indem man auf list prüft) ein krasses Antipattern ist? und dass dir das auch jeder so um die Ohren hauen würde? Wieso dann in C++ schlechter machen?



  • @PadMad sagte in Array übergeben zu Konstruktor:

    Und ich persönlich sehe hier optisch durchaus einen Unterschied, was die Anzahl der Elemente betrifft... vielleicht habe ich mich aber auch verzählt...
    std::array<std::string, 7> _weekDays = {"Mo","Di","Mi","Do","Fr","Sa","So"};
    std::vectorstd::string _weekDaysAndMore = {"Mo","Di","Mi","Do","Fr","Sa","So","1","42","0xfff","g"};

    Du initialisierst doch hier den vector mit Absicht falsch 🙄

    Wie ich schon oben sagte: wenn ich zu blöd bin, den vector richtig zu initialisieren, dann schützt mich auch niemand vor:

    std::array<std::string, 9> _weekDays = {"Mo","Di","Mi","Do","Fr","Sa","So"};
    

    @PadMad sagte in Array übergeben zu Konstruktor:

    Aber was möchtest Du mir jetzt damit sagen? Dass, wenn Jemand von einem c-array redet, man durchaus den Vorschlag machen darf, dass std::vector eine gute Alternative ist, std::array aber nicht?

    Das will ich (natürlich) nicht damit sagen.
    Ich bin lediglich der Meinung, dass der vector nicht schlechter ist als das std::array, er aber leichter handhabbar ist.
    Und dass die Tatsache, dass der TE nach (c-)array gefragt hat, nicht zwangsläufig zu std::array führen muss ... Du hattest ja argumentiert:

    @PadMad sagte in Array übergeben zu Konstruktor:

    Weil letzten Endes hat @Johnny01 gefragt wie man ein "array" an einen Konstruktor übergeben kann.



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



  • @Belli sagte in Array übergeben zu Konstruktor:

    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. Bzw. es lässt sich nicht machen, weil der Compiler Fehler meldet. Nach Möglichkeit sollte man aber entsprechende STL-Funktionen benutzen und nur die Operation bereitstellen (std::accumulate, std::for_each, std::transform und Konsorten).


  • 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);
    }