Type Erasure Idiom



  • Bin über nen Blog Eintrag gestolpert, in dem das Type Erasure Idiom erweitert wird.

    Könnt ihr mal nen Block drüber werfen und mir sagen, ob das was taugt?

    Meiner Meinung nach sieht das ziemlich messy aus

    #include <iostream>
    #include <memory>
    #include <string>
    
    namespace detail {
    
    template <class T>
    class Holder {
      public:
        using Data = T;
    
        Holder(T obj) : data_(std::move(obj)) {}
        virtual ~Holder() = default;
        T &get() { return data_; }
        const T &get() const { return data_; }
    
      private:
        T data_;
    };
    
    template <class Concept_, template <class> class Model>
    class Container {
      public:
        using Concept = Concept_;
    
        template <class T>
        Container(T obj)
            : self_(std::make_shared<Model<Holder<T>>>(std::move(obj))) {}
    
        const Concept &get() const { return *self_.get(); }
    
      private:
        std::shared_ptr<const Concept> self_;
    };
    
    // Helpers for spec merging.
    template <class Spec>
    using ConceptOf = typename Spec::Concept;
    template <class Spec, class Holder>
    using ModelOf = typename Spec::template Model<Holder>;
    template <class Spec, class Container>
    using ExternalInterfaceOf =
        typename Spec::template ExternalInterface<Container>;
    template <class Spec>
    using ContainerOf =
        detail::Container<typename Spec::Concept, Spec::template Model>;
    
    } // namspace detail
    
    template <class Spec_>
    class TypeErasure
        : public detail::ExternalInterfaceOf<Spec_, detail::ContainerOf<Spec_>> {
        using Base =
            detail::ExternalInterfaceOf<Spec_, detail::ContainerOf<Spec_>>;
    
      public:
        using Base::Base;
        using Spec = Spec_;
    };
    
    template <class SpecA, class SpecB>
    struct MergeSpecs {
        struct Concept : public virtual detail::ConceptOf<SpecA>,
                         public virtual detail::ConceptOf<SpecB> {};
    
        template <class Holder>
        struct Model
            : public detail::ModelOf<SpecA, detail::ModelOf<SpecB, Holder>>,
              public virtual Concept {
            using Base = detail::ModelOf<SpecA, detail::ModelOf<SpecB, Holder>>;
            using Base::Base;
        };
    
        template <class Container>
        struct ExternalInterface
            : public detail::ExternalInterfaceOf<
                  SpecA, detail::ExternalInterfaceOf<SpecB, Container>> {
    
            using Base = detail::ExternalInterfaceOf<
                SpecA, detail::ExternalInterfaceOf<SpecB, Container>>;
            using Base::Base;
        };
    };
    
    struct GreeterSpec {
        struct Concept {
            virtual ~Concept() = default;
            virtual void greet(const std::string &name) const = 0;
        };
    
        template <class Holder>
        struct Model : public Holder, public virtual Concept {
            using Holder::Holder;
            virtual void greet(const std::string &name) const override {
                this->Holder::get().greet(name);
            }
        };
    
        template <class Container>
        struct ExternalInterface : public Container {
            using Container::Container;
            void greet(const std::string &name) const {
                this->Container::get().greet(name);
            }
        };
    };
    
    struct OpenerSpec {
        struct Concept {
            virtual ~Concept() = default;
            virtual void open() const = 0;
        };
    
        template <class Holder>
        struct Model : public Holder, public virtual Concept {
            using Holder::Holder;
            virtual void open() const override {
                this->Model::get().open();
            }
        };
    
        template <class Container>
        struct ExternalInterface : public Container {
            using Container::Container;
            void open() const {
                this->Container::get().open();
            }
        };
    };
    
    using Greeter = TypeErasure<GreeterSpec>;
    using Opener = TypeErasure<OpenerSpec>;
    using OpenerAndGreeter = TypeErasure<MergeSpecs<OpenerSpec, GreeterSpec>>;
    
    class English {
      public:
        void greet(const std::string &name) const {
            std::cout << "Good day " << name << ". How are you?\n";
        }
        void open() const {
            std::cout << "Squeak...\n";
        }
    };
    
    class French {
      public:
        void greet(const std::string &name) const {
            std::cout << "Bonjour " << name << ". Comment ca va?\n";
        }
        void open() const {
            std::cout << "Couic...\n";
        }
    };
    
    void open_door(const Opener &o) {
        o.open();
    }
    
    void greet_tom(const Greeter &g) {
        g.greet("Tom");
    }
    
    void open_door_and_greet_john(const OpenerAndGreeter &g) {
        g.open();
        g.greet("John");
    }
    
    int main() {
        English en;
        French fr;
    
        open_door(en);
        open_door(fr);
        std::cout << "----------------\n";
        greet_tom(en);
        greet_tom(fr);
        std::cout << "----------------\n";
        open_door_and_greet_john(en);
        open_door_and_greet_john(fr);
    }
    


  • Was genau findest du daran messy?



  • wirkt auf mich ein bisschen so, bspw. das hier

    template <class Concept_, template <class> class Model> 
    class Container { 
      public: 
        using Concept = Concept_; 
    
        template <class T> 
        Container(T obj) 
            : self_(std::make_shared<Model<Holder<T>>>(std::move(obj))) {} 
    
        const Concept &get() const { return *self_.get(); } 
    
      private: 
        std::shared_ptr<const Concept> self_; 
    };
    

    oder die ganzen using directiven

    was haltet ihr von dem Konzept an sich?


  • Mod

    Nochmal: Was genau findest du daran messy?



  • ich würde meinen, dass dieser Code eher schwer nachzuvollziehen ist oder wie Sean Parent sagt "no local reasoning"



  • Ja, ist relativ schwer nachzuvollziehen. Zumindest wenn man nicht gewohnt ist solchen Code zu lesen (was ich z.B. nicht bin).
    Aber das macht ja nix.
    Wichtig ist dass der Teil ab und inklusive struct GreeterSpec halbwegs einfach zu schreiben und zu lesen ist - und das ist mMn. gegeben.

    Der Teil davor ist sind Hilfsklassen die man 1x schreibt und dann bloss noch verwenden. Der muss nicht flüssig zu lesen sein. Sobald der fehlerfrei ist muss man sich darum nicht mehr kümmern.

    Guck dir mal eine Standard Library Implementierung an oder die Implementierung diverser Boost Libraries. Die sind auch sehr schwer zu lesen wenn man keine Übung darin hat. Trotzdem sind diese Libraries sehr wertvoll, weil sie einem bestimmte Dinge enorm erleichtern.

    Der Code so wie er da steht ist ja bloss ein Beispiel. Wenn man genau nur die beiden hier gezeigten concepts "type erasen" will, dann tut's vermutlich auch eine nicht generische Lösung, die dann entsprechend einfacher ausfällt. Die Intention dieses Codes ist es aber eine generische Lösung anzubieten, die dann pro concepts deutlich weniger Code benötigt und es vor allem auch ermöglicht concepts zu "mergen".



  • Abgefahren! Ich kenne zwar Sean Parent's "Concept-based Polymorphism"-Vortrag, aber das hier setzt nochmal einen open drauf! Ja, da muss man schon eine Weile drauf gucken, um es zu verstehen, was da passiert. Immerhin muss man so nur noch so eine "Spec" schreiben. Ich hoffe aber, dass man mit besseren Meta-Programmier-Fähigkeiten (meta classes?) irgendwann nur noch ein Concept/Interface definieren muss, und dass dann der andere Kram so einer "spec" automatisch generiert werden kann.

    BTW: So sieht das in Rust aus. SCNR!



  • Danke für eure Einschätzungen.

    Was für einen Vorteil hat eigentlich das Erben von Konstruktoren aus Basisklassen?



  • Ich habe momentan noch Verstänndnisprobleme mit beispielsweise dieser Zeile

    template <class Concept_, template <class> class Model> 
    class Container { 
    ..
    };
    

    was geschieht hier?



  • kann mir jemand schnell sagen wie so eine Zeile hier zu verstehen ist?

    template <class Concept, template <class> class Model>
    class XYZ {
    ...
    };
    

    danke schonmal



  • Das zweite ist ein sog. "Template template parameter", welcher besagt, daß nicht ein beliebiger Datentyp, sondern nur ein Template mit in diesem Fall genau einem Parameter beim Instantiieren übergeben werden muß, z.B.

    Container<X, Y<Z>>
    

    s.a. Template parameters and template arguments

    PS: Wie überall bei Templates kann man statt "class" auch "typename" schreiben:

    template <typename Concept_, template <typename> typename Model>
    class Container
    

    Dies finde ich persönlich besser, da ja nicht unbedingt immer eine Klasse angegeben werden muß.



  • Th69 schrieb:

    Wie überall bei Templates kann man statt "class" auch "typename" schreiben:

    Jo aber für template-template-Parameter erst seit C++17. Gewisse Distros sitzen immer noch auf GCC 4...



  • Vielen Dank!



  • Fytch: hast recht, hier nochmal mit C++14 (und früher):

    template <typename Concept_, template <typename> class Model>
    class Container
    

    (also "typename" nur bei den eigentlichen Template-Parametern, welche selbst kein "template" voranstehen haben).



  • Ich betrachte gerade ein leicht vereinfachtes Beispiel

    #include <iostream>
    #include <memory>
    #include <string>
    
    template <class T>
    class Holder {
     public:
      Holder(T obj) : data_(std::move(obj)) {}
      virtual ~Holder() = default;
      T& get() { return data_; }
      const T& get() const { return data_; }
    
     private:
      T data_;
    };
    
    // Das zweite ist ein sog. "Template template parameter", welcher besagt, daß
    // nicht ein beliebiger Datentyp, sondern nur ein Template mit in diesem Fall
    // genau einem Parameter beim Instantiieren übergeben werden muß
    template <class Concept, template <class> class Model>
    class Container {
     public:
      template <class T>
      Container(T obj)
          : self_(std::make_shared<Model<Holder<T>>>(std::move(obj))) {}
    
      const Concept& get() const { return *self_.get(); }
    
     private:
      std::shared_ptr<const Concept> self_;
    };
    
    template <class Spec>
    struct TypeErasure
        : Spec::template ExternalInterface<
              Container<typename Spec::Concept, Spec::template Model>> {
      using Base = typename Spec::template ExternalInterface<
          Container<typename Spec::Concept, Spec::template Model>>;
      using Base::Base;
    };
    ////////////////////////////////////////////////////
    
    struct GreeterSpec {
      struct Concept {
        virtual ~Concept() = default;
        virtual void greet(const std::string& name) const = 0;
      };
    
      template <class Holder>
      struct Model : public Holder, public virtual Concept {
        using Holder::Holder;
        virtual void greet(const std::string& name) const override {
          this->Holder::get().greet(name);
        }
      };
    
      // The base-class is determined only when the type of ExternalInterface is
      // defined
      template <class BaseClass>
      struct ExternalInterface : public BaseClass {
        using BaseClass::BaseClass;
        void greet(const std::string& name) const {
          this->BaseClass::get().greet(name);
        }
      };
    };
    
    using Greeter = TypeErasure<GreeterSpec>;
    

    habe noch ein paar Probleme mit dem Verständnis dieser Zeile

    struct TypeErasure
        : Spec::template ExternalInterface<
              detail::Container<typename Spec::Concept, Spec::template Model>> {
    ...
     };
    

    Ich verstehe es so, dass TypeErasure von einer nested template class (ExternalInterface) seines übergegebenen Type Parameter erbt. Da ExternalInterface eine Template-class ist, steht da hinter dem ScopeResolution operator noch day keyword template.

    Spec::template
    

    ExternalInterface erbt von seinem template paramter und wird mit

    ExternalInterface<Container<typename Spec::Concept, Spec::template Model>>
    

    instantiiert. Container wiederum ist auch eine template-class mit zwei Typenparemetern,, wovon der zweite ein Template template parameter ist und die template-class Model spezifiziert bekommt. Soweit korrekt?

    Wozu brauche ich noch das keyword typename bei der Angabe des ersten template parameters?

    Und wieso muss beim zweiten template parameter

    Spec::template Model
    

    kein template Parameter für Model festgelegt werden?


Log in to reply