Irgendwie generische Schnittstelle, oder auch nicht.



  • vll schrieb:

    [...]
    int main()
    {
      std::vector<Bad> v(10);
      foo(v.begin(), v.end()); // Fail. Und es gibt nicht mal einen Compiler-Fehler.
    }
    

    Oder übersehe ich da was?

    Was soll denn sonst passieren? Was gibt es denn besseres als einen Fehler zur Compile-Zeit?

    vll schrieb:

    Und auch wenn man bei diesem Beispiel schon Pech haben muss, mein Problem mit dieser Methode ist einfach, dass man der Funktion quasi jeden "Mist" übergeben kann,

    Geht ja nicht. Hast Du selbst gezeigt.

    vll schrieb:

    wäre es nicht schöner, wenn man irgendwie von außen zeigen könnte, dass sie nur für eine bestimmte Klasse ist?

    Dafür ist Dokumentation gut. In Form von selbstdokumentierenden C++ Code lässt sich das noch nicht ausdrücken -- es sei denn, du zählst das "SFINAE" constraining dazu (siehe mein vorheriges Beispiel). Aber so leserlich ist das nicht wirklich. Man kann es aber dazu verwenden, Compiler-Fehlermeldungen zu kürzen und Überladungs-Mehrdeutigkeiten zu entfernen.

    Du bist aber nicht der einzige, der diese Bedingungen "nativ" ausdrücken können möchte. Beispiel:

    template<[b]InputIterator[/b] Iter>
    [b]requires SameType<Iter::value_type,type_t>[/b]
    void do_smthing(Iter beg, Iter end)
    {
    ___
    }
    

    So eine Spracherweiterung ist aber noch nicht 100% zu Ende gedacht. Vielleicht kommt das irgendwann nochmal beim nächsten Sprachstandard.



  • krümelkacker schrieb:

    Was soll denn sonst passieren? Was gibt es denn besseres als einen Fehler zur Compile-Zeit?

    Einen Fehler vor der Template-Auflösung. Aber ich glaube, dir ist nicht bewusst, dass das Beispiel oben anstandslos kompiliert? 🙂



  • vll schrieb:

    krümelkacker schrieb:

    Was soll denn sonst passieren? Was gibt es denn besseres als einen Fehler zur Compile-Zeit?

    Einen Fehler vor der Template-Auflösung. Aber ich glaube, dir ist nicht bewusst, dass das Beispiel oben anstandslos kompiliert? 🙂

    Richtig. Ich habe es nur überflogen und Deinen "Fail"-Kommentar als Compile-Zeit-Fehler interpretiert. Nächste Frage: Ist es schlimm, dass es kompiliert?

    Wie gesagt: Constraining geht -- aber eben mit Tricks statt direkter Sprachunterstützung.



  • Entweder einfach davon ausgehen, dass der Iterator-value_type zu dem passt, was du machen willst. Da könnten dann natürlich nicht nur Iteratoren auf type_t funktionieren, sondern auch auf something_like_type_t, wenn der die nötige Schnittstelle anbietet. Das wäre dann ein Fall von statischer Polymorphie.

    struct type_t
    {
      void smth()
      {
        std::cout << "I am a type_t\n";
      }
    };
    
    struct something_like_type_t
    {
      int smth() const
      {
        std::cout << "No type_t, but can do smth, too\n";
        return 1;
      }
    };
    
    template <class Iterator>
    void do_smth_with_me(Iterator first, Iterator last)
    {
      while (first != last)
        (first++)->smth();
    }
    
    int main()
    {
      std::vector<type_t> real(3);
      std::deque<something_like_type_t> fake(2);
    
      do_smth_with_me(real.begin(), real.end());
      do_smth_with_me(fake.rbegin(), fake.rend());
    }
    

    Alternativ, wenn du wirklich nur type_t als value_type zulassen willst, kannst du das je nach C++-Version absichern:

    #include <iterator>
    #if defined(HAS_0X_TYPE_TRAITS)
      #include <type_traits> //C++0x
      using std::is_same; //C++0x
    #elif defined(HAS_TR1_TYPE_TRAITS)
      #include <type_traits> //TR1
      using std::tr1::is_same; //TR1
    #endif
    
    #ifdef HAS_NO_0X_STATIC_ASSERT
      #include <boost/static_assert.hpp> //boost
    #endif
    
    void do_smth_with_me(Iterator first, Iterator last)
    {
      const static bool is_type_t = is_same<type_t,std::iterator_traits<Iterator>::value_type>::value;
      static_assert(is_type_t, "must be type_t"); //C++0x
      BOOST_STATIC_ASSERT_MSG(is_type_t, "must be type_t"); //boost
      while (first != last)
        (first++)->smth();
    }
    

    Die HAS_xy-Makros hab ich frei erfunden, es gibt aber z.B. in Boost.Configuration ähnliches.



  • inline void check_good(const Good &)
    {
    }
    
    template<class InputIterator> 
    void foo(InputIterator first, InputIterator last) 
    { // Ist nur für Good gedacht. 
      for (; first != last; ++first); 
      {
        check_good(*first);
        first->FooBar(); 
      }
    } 
    
    int main() 
    { 
      std::vector<Bad> v(10); 
      foo(v.begin(), v.end()); //Compile-Fehler ohne Boost und Template-Gebastel
    }
    


  • Mir ist noch etwas eingefallen. Es gibt auch "polymorphe" Iteratoren. Das, was da per type-eraures versteckt wird, ist der Container-Typ, nicht aber der Element-Typ.

    void do_something(any_input_iter<const type_t> begin, any_input_iter<const type_t> end);
    

    wobei dann "any_inut_iter" ein Klassen-Template ist, was den tatsächlichen Iterator-Typ versteckt. Für Iteratoren ist das aber recht unpopulär. Bei Funktionsobjekten sieht das schon anders aus:

    void function_sink(boost::function<double(double,double)> func)
    {
      double x = func(2.7,3.1);
      ...
    }
    


  • TyroXx hat mich auf eine Idee gebracht:

    template<class InputIterator>
    void foo(InputIterator first, InputIterator last)
    { // Ist nur für Good gedacht.
      for (; first != last; ++first);
        Good(*first).FooBar();
    }
    

    Die Fehlermeldung ist auch gut:

    cannot convert from 'Bad' to 'Good'

    Edit:
    Wobei man Annahmen über den Konstruktor machen muss..



  • Solche Templates benutze ich in normalen Projekten (also keine headeronly Bibliothek o.ä) sowieso fast nur als kleine Helfer, die eigentlich nichts machen, als die für jedes Element die eigentliche Methode aufzurufen, die dann in der cpp definiert ist (oder einen insert in einen STL Container o.ä.). Die eigentliche Implementierung sollte i.d.R. nicht in irgendwelchen öffentlichen Headern herumschwirren. Damit hat sich das "Problem" auch ergeben, dass man irgendeinen Müll übergeben kann (ich find dieses Tatsache auch nicht so wahnsinnig problematisch).

    Dein Beispiel:

    struct Good
    {
      void FooBar()
      {
        std::cout << "Hallo!\n";
      }
    };
    
    struct Bad
    {
      void FooBar()
      {
        std::cout << "Fail!\n";
      }
    };
    
    // myclass.hpp
    class MyClass
    {
    public:
      template<class InputIterator>
      void foo(InputIterator first, InputIterator last)
      { // Ist nur für Good gedacht.
        for (; first != last; ++first);
          foo(*first);  // geht nur mit Good
      }
      template<class InputIterator>
      void add(InputIterator first, InputIterator last)
      { // Ist nur für Good gedacht.
        myGoods.insert(myGoods.end(), first, last);  // geht nur mit Good
      }
    private:
      void foo(Good&);
      std::vector<Good> myGoods;
    };
    


  • Je nachdem, was du da machen willst, solltest du noch das Semikolon hinter der for-Schleife entfernen:

    template<class InputIterator>
      void foo(InputIterator first, InputIterator last)
      { // Ist nur für Good gedacht.
        for (; first != last; ++first);  // <-- hier!
          foo(*first);  // geht nur mit Good
      }
    


  • pumuckl schrieb:

    ... Das wäre dann ein Fall von statischer Polymorphie.

    struct type_t
    {
      void smth()
      {
        std::cout << "I am a type_t\n";
      }
    };
    
    struct something_like_type_t
    {
      int smth() const
      {
        std::cout << "No type_t, but can do smth, too\n";
        return 1;
      }
    };
    
    template <class Iterator>
    void do_smth_with_me(Iterator first, Iterator last)
    {
      while (first != last)
        (first++)->smth();
    }
    
    int main()
    {
      std::vector<type_t> real(3);
      std::deque<something_like_type_t> fake(2);
    
      do_smth_with_me(real.begin(), real.end());
      do_smth_with_me(fake.rbegin(), fake.rend());
    }
    

    (Info: bin noch nicht so lange mit C++ zu gange, komme aus der C#-Welt)
    Ist das überhaupt ein guter Programmier-Stil? Auf den ersten Blick sah das für mich nach Duck Typing aus und statische-polymorphie war vorher für mich noch nicht einmal ein begriff.
    Also, meine eigentliche frage: Gilt sowas überhaupt als Schnittstelle? eigentlich ist das doch nur ein Compiler-Feature, welches bei nicht 100% Know-How nur zu solch unnötig komplizierten Fehlern/Fragen kommt?!
    Meiner Meinung nach (lasse mich natürlich übereden!) ist die Verwendung durch dynamische Polymorphie (sprich, die Verwendung von "richtigen" Schnittstellen) die bessere Variante, oder nich?



  • krümelkacker schrieb:

    Mir ist noch etwas eingefallen. Es gibt auch "polymorphe" Iteratoren. Das, was da per type-eraures versteckt wird, ist der Container-Typ, nicht aber der Element-Typ.

    void do_something(any_input_iter<const type_t> begin, any_input_iter<const type_t> end);
    

    (...)

    Was mich dabei stört, ist, dass die Performance gröber darunter leidet.
    Iteratoren werden überall als "cheap to copy" verwendet, da normale Iteratoren eben auch billig zu kopieren sind.

    Bloss polymorphe Iteratoren sind nicht billig zu kopieren. Zumindest dann nicht, wenn sie keine "Small Object Optimization" ala boost::function enthalten, und die ist leider gar nicht trivial korrekt zu implementieren.

    Ich hab' daher in meinen Projekten bisher immer abstrakte "Enumeratoren" verwendet:

    template <class T>
    class AbstractEnumerator
    {
    public:
        virtual void Reset() = 0;
        virtual void MoveNext() = 0;
    
        virtual bool HasCurrent() const = 0;
        virtual T& GetCurrent() const = 0;
    };
    
    template <class T, class I>
    class IteratorPairEnumerator
    {
        // ...
    };
    
    void do_something(AbstractEnumerator<const type_t>& enumerator);
    

    Das ist jetzt nicht so "C++ig", aber ist einfach umzusetzen und funktioniert.



  • Sicher, man kann nicht alles haben. Ich bin da jetzt auch kein Fan von solchen Iteratoren. Spätestens der Vergleich current!=end wird ekelig, wenn man erstmal per typeid testen muss, ob die überhaupt kompatibel sind. Da ist die Abstraktion "C++ Iterator" nicht die beste.

    Wenn ich mich richtig erinnere, müsste ich mal zum Spaß eine "any_range" nach dem range-Konzept von Alexandrescu geschrieben haben.



  • krümelk_im_cafe schrieb:

    Sicher, man kann nicht alles haben. Ich bin da jetzt auch kein Fan von solchen Iteratoren. Spätestens der Vergleich current!=end wird ekelig, wenn man erstmal per typeid testen muss, ob die überhaupt kompatibel sind.

    Wieso, der type_info Vergleich ist ja kein Problem.

    Ich gehe davon aus, dass alle vernünftigen Compiler erstmal checken ob die Adresse der beiden type_info Instanzen gleich ist, "succeed fast" sozusagen. Und dann erst anfangen die Namens-Strings oder was auch immer zu vergleichen.

    Falls man wirklich ein System hat wo der Vergleich zu lange dauert, kann man den "succees fast" Teil selbst basteln: Type-ID-Helper-Template das ne statische Variable hält, die Adresse dieser verwendet man als "fast typeid". Normalerweise würde das schon ausreichen, aber da man vermutlich auch Systeme unterstützen will die sowas wie DLLs kennen, macht man im "Adressen ungleich" Fall noch einen zusätzlichen Test über typeid().

    Da der "Adressen gleich" Fall der interessante ist (weil genau der am häufigsten vorkommen wird), sollte das reichen.

    Ganz abgesehen davon dass es AFAIK sowieso UB ist zwei Iteratoren zu vergleichen die nicht vom selben Container kommen. D.h. man müsste diese Tests nur in Debug-Builds machen - Release-Builds lässt man einfach schön in UB reinlaufen.


Anmelden zum Antworten