wake-up-neo.com

So erreichen Sie eine "virtuelle Vorlagenfunktion" in C++

zunächst einmal: Ich habe gelesen und weiß jetzt, dass eine virtuelle Template-Member-Funktion (noch nicht?) in C++ möglich ist. Eine Problemumgehung besteht darin, aus der Klasse eine Vorlage zu machen und dann das Vorlagenargument auch in der Member-Funktion zu verwenden.

Im Zusammenhang mit OOP finde ich jedoch, dass das folgende Beispiel nicht sehr "natürlich" wäre, wenn die Klasse tatsächlich eine Vorlage wäre. Bitte beachten Sie, dass der Code eigentlich nicht funktioniert, aber der gcc-4.3.4 berichtet: error: templates may not be ‘virtual’

#include <iostream>
#include <vector>

class Animal {
    public:
        template< class AMOUNT >
        virtual void eat( AMOUNT amount ) const { 
            std::cout << "I eat like a generic Animal." << std::endl; 
        }
        virtual ~Animal() { 
        }
};

class Wolf : public Animal {
    public:
        template< class AMOUNT >
        void eat( AMOUNT amount) const { 
            std::cout << "I eat like a wolf!" << std::endl; 
        }
        virtual ~Wolf() { 
        }
};

class Fish : public Animal {
    public:
        template< class AMOUNT >
        void eat( AMOUNT amount) const { 
            std::cout << "I eat like a fish!" << std::endl; 
        }
        virtual ~Fish() { 
        }
};

class GoldFish : public Fish {
    public:
        template< class AMOUNT >
        void eat( AMOUNT amount) const { 
            std::cout << "I eat like a goldfish!" << std::endl; 
        }
        virtual ~GoldFish() { 
        }
};

class OtherAnimal : public Animal {
        virtual ~OtherAnimal() { 
        }
};

int main() {
    std::vector<Animal*> animals;
    animals.Push_back(new Animal());
    animals.Push_back(new Wolf());
    animals.Push_back(new Fish());
    animals.Push_back(new GoldFish());
    animals.Push_back(new OtherAnimal());

    for (std::vector<Animal*>::const_iterator it = animals.begin(); it != animals.end(); ++it) {
        (*it)->eat();
        delete *it;
    }

    return 0;
}

Ein "Fish <Amount> Foo" zu erstellen, ist also irgendwie seltsam. Es erscheint mir jedoch wünschenswert, jedem Tier eine willkürliche Futtermenge zu geben.

Daher suche ich nach einer Lösung, wie man so etwas erreichen kann

Fish bar;
bar.eat( SomeAmount food );

Dies ist besonders nützlich, wenn Sie die for-Schleife betrachten. Man könnte allen Tieren eine bestimmte Menge (FoodAmount) zuführen (z. B. über eat () und bind1st ()), dies könnte jedoch nicht so einfach erfolgen, obwohl ich dies für sehr verwundend halte (und daher bis zu einem gewissen Grad) "natürlich". Während einige jetzt argumentieren wollen, dass dies auf den "einheitlichen" Charakter eines Vektors zurückzuführen ist, denke ich/wünsche ich mir, dass dies möglich sein sollte, und ich würde wirklich gerne wissen, wie das ist verwirrt mich schon seit geraumer Zeit ...

[BEARBEITEN]

Um vielleicht die Motivation für meine Frage zu klären, möchte ich eine Exporter-Klasse programmieren und verschiedene, spezialisiertere Klassen daraus ableiten lassen. Während die Exporter-Klasse der obersten Ebene im Allgemeinen nur kosmetischen/strukturellen Zwecken dient, wird eine GraphExporter-Klasse abgeleitet, die wiederum als Basisklasse für einen noch spezialisierteren Export dienen sollte. Ähnlich wie im Animal-Beispiel möchte ich GraphExporter * auch für spezialisierte/abgeleitete Klassen definieren können (z. B. für SpecialGraphExplorer). Beim Aufruf von "write (out_file)" sollte stattdessen die entsprechende Member-Funktion für SpecialGraphExporter aufgerufen werden von GraphExporter :: write (out_file).

Vielleicht werden dadurch meine Situation und meine Absichten klarer.

Beste,

Schatten

34
Shadow

Nach einigem Überlegen erkannte ich dies als die klassische Multimethodenanforderung , dh eine Methode, die basierend auf dem Laufzeittyp ausgelöst wird von mehr als einem Parameter. Übliche virtuelle Funktionen sind single dispatch im Vergleich (und sie werden nur nach dem Typ this versendet).

Beziehen Sie sich auf Folgendes:

  • Andrei Alexandrescu hat (die wegweisenden Bits für C++?) Über die Implementierung von Multi-Methoden unter Verwendung von Generika in 'Modern C++ Design' Geschrieben.
    • Kapitel 11: "Multimethoden" - Es implementiert grundlegende Multimethoden, um sie logarithmisch zu machen (unter Verwendung geordneter Typlisten) und dann bis hin zu Multimethoden mit konstanter Zeit. Ziemlich mächtiges Zeug!
  • Ein codeproject article , der anscheinend eine solche Implementierung hat:
    • keine Verwendung von Typumwandlungen jeglicher Art (dynamisch, statisch, neu interpretiert, const oder C-Stil)
    • keine Verwendung von RTTI;
    • keine Verwendung eines Präprozessors;
    • starke Schriftsicherheit;
    • separate Zusammenstellung;
    • konstante Zeit für die Ausführung mehrerer Methoden;
    • keine dynamische Speicherzuweisung (über new oder malloc) während eines Multimethod-Aufrufs;
    • keine Verwendung von Nicht-Standard-Bibliotheken;
    • es werden nur Standardfunktionen von C++ verwendet.
  • C++ Open Method Compiler , Peter Pirkelbauer, Yuriy Solodkyy und Bjarne Stroustrup
  • Die Loki-Bibliothek hat Ein MultipleDispatcher
  • Wikipedia hat ein schönes einfaches Schreiben mit Beispielen zu Multiple Dispatch in C++.

Hier ist der 'einfache' Ansatz aus dem Wikipedia-Artikel als Referenz (der weniger einfache Ansatz skaliert besser für eine größere Anzahl abgeleiteter Typen):

// Example using run time type comparison via dynamic_cast

struct Thing {
    virtual void collideWith(Thing& other) = 0;
}

struct Asteroid : Thing {
    void collideWith(Thing& other) {
        // dynamic_cast to a pointer type returns NULL if the cast fails
        // (dynamic_cast to a reference type would throw an exception on failure)
        if (Asteroid* asteroid = dynamic_cast<Asteroid*>(&other)) {
            // handle Asteroid-Asteroid collision
        } else if (Spaceship* spaceship = dynamic_cast<Spaceship*>(&other)) {
            // handle Asteroid-Spaceship collision
        } else {
            // default collision handling here
        }
    }
}

struct Spaceship : Thing {
    void collideWith(Thing& other) {
        if (Asteroid* asteroid = dynamic_cast<Asteroid*>(&other)) {
            // handle Spaceship-Asteroid collision
        } else if (Spaceship* spaceship = dynamic_cast<Spaceship*>(&other)) {
            // handle Spaceship-Spaceship collision
        } else {
            // default collision handling here
        }
    }
}
31
sehe

Offensichtlich sind virtuelle Elementfunktionsvorlagen nicht zulässig und konnten auch theoretisch nicht realisiert werden. Um eine virtuelle Tabelle einer Basisklasse zu erstellen, muss eine begrenzte Anzahl virtueller Funktionszeigereinträge vorhanden sein. Eine Funktionsvorlage würde eine unbegrenzte Anzahl von "Überladungen" (d. H. Instantiierungen) zulassen.

Theoretisch könnte eine Sprache (wie C++) virtuelle Member-Funktionsvorlagen zulassen, wenn sie über einen Mechanismus zur Angabe der tatsächlichen (endlichen) Liste von Instantiierungen verfügt. C++ hat diesen Mechanismus (d. H. Explizite Template-Instantiierungen). Ich vermute, dass dies in einem neueren C++ - Standard möglich ist (obwohl ich keine Ahnung habe, welche Schwierigkeiten Compiler-Hersteller damit haben würden, diese Funktion zu implementieren). Aber das ist nur eine theoretische Diskussion, in der Praxis ist dies einfach nicht erlaubt. Die Tatsache bleibt, Sie müssen die Anzahl der virtuellen Funktionen endlich machen (keine Vorlagen zulässig).

Das bedeutet natürlich nicht, dass Klassenvorlagen keine virtuellen Funktionen haben können, und dass virtuelle Funktionen keine Funktionsvorlagen aufrufen können. Es gibt also viele Lösungen in dieser Richtung (wie das Besuchermuster oder andere Schemata).

Eine Lösung, die Ihrer Meinung nach (obwohl schwer zu verstehen) elegant dient, ist folgende (die im Wesentlichen ein Besuchermuster ist):

#include <iostream>
#include <vector>

struct Eater { 
  virtual void operator()(int amount) const = 0;
  virtual void operator()(double amount) const = 0;
};

template <typename EaterType>
struct Eater_impl : Eater {
  EaterType& data;
  Eater_impl(EaterType& aData) : data(aData) { };
  virtual void operator()(int amount) const { data.eat_impl(amount); };
  virtual void operator()(double amount) const { data.eat_impl(amount); };
};

class Animal {
  protected:
    Animal(Eater& aEat) : eat(aEat) { };
  public:
    Eater& eat;
    virtual ~Animal() { delete &eat; };
};

class Wolf : public Animal {
  private:
    template< class AMOUNT >
    void eat_impl( AMOUNT amount) const { 
      std::cout << "I eat like a wolf!" << std::endl; 
    }

  public:
    friend struct Eater_impl<Wolf>;        
    Wolf() : Animal(*(new Eater_impl<Wolf>(*this))) { };
    virtual ~Wolf() { };
};

class Fish : public Animal {
  private:
    template< class AMOUNT >
    void eat_impl( AMOUNT amount) const { 
      std::cout << "I eat like a fish!" << std::endl; 
    }
  public:
    friend struct Eater_impl<Fish>;
    Fish() : Animal(*(new Eater_impl<Fish>(*this))) { };
    virtual ~Fish() { };
};

int main() {
  std::vector<Animal*> animals;
  animals.Push_back(new Wolf());
  animals.Push_back(new Fish());

  for (std::vector<Animal*>::const_iterator it = animals.begin(); it != animals.end(); ++it) {
    (*it)->eat(int(0));
    (*it)->eat(double(0.0));
    delete *it;
  };

  return 0;
};

Das Obige ist eine nette Lösung, da Sie damit eine begrenzte Anzahl von Überladungen definieren können, die Sie nur an einer Stelle benötigen (in der Eater_impl-Klassenvorlage). Alles, was Sie in der abgeleiteten Klasse benötigen, ist eine Funktionsvorlage (und möglicherweise zusätzliche Überladungen) Sonderfälle). Es gibt natürlich ein bisschen Overhead, aber ich denke, es könnte ein bisschen mehr in den Kopf gesetzt werden, um den Overhead zu reduzieren (zusätzlicher Referenzspeicher und dynamische Zuordnung von Eater_impl). Ich denke, das merkwürdig wiederkehrende Schablonenmuster könnte wahrscheinlich irgendwie dazu verwendet werden.

11
Mikael Persson

Ich denke, das Besuchermuster kann eine Lösung sein.

UPDATE

Ich habe mein Beispiel beendet:

#include <iostream>
#include <vector>
#include <boost/shared_ptr.hpp>

class Animal;
class Wolf;
class Fish;

class Visitor
{
    public:
    virtual void visit(const Animal& p_animal) const = 0;
    virtual void visit(const Wolf& p_animal) const = 0;
    virtual void visit(const Fish& p_animal) const = 0;
};

template<class AMOUNT>
class AmountVisitor : public Visitor
{
    public:
    AmountVisitor(AMOUNT p_amount) : m_amount(p_amount) {}
    virtual void visit(const Animal& p_animal) const
    {
        std::cout << "I eat like a generic Animal." << std::endl;
    }
    virtual void visit(const Wolf& p_animal) const
    {
        std::cout << "I eat like a wolf!" << std::endl;
    }
    virtual void visit(const Fish& p_animal) const
    {
        std::cout << "I eat like a fish!" << std::endl;
    }


    AMOUNT m_amount;
};

class Animal {
    public:

        virtual void Accept(const Visitor& p_visitor) const
        {
            p_visitor.visit(*this);
        }

        virtual ~Animal() {
        }
};

class Wolf : public Animal {
    public:
        virtual void Accept(const Visitor& p_visitor) const
        {
            p_visitor.visit(*this);
        }
};

class Fish : public Animal {
    public:
        virtual void Accept(const Visitor& p_visitor) const
        {
            p_visitor.visit(*this);
        }
};

int main()
{
    typedef boost::shared_ptr<Animal> TAnimal;
    std::vector<TAnimal> animals;
    animals.Push_back(TAnimal(new Animal()));
    animals.Push_back(TAnimal(new Wolf()));
    animals.Push_back(TAnimal(new Fish()));

    AmountVisitor<int> amount(10);

    for (std::vector<TAnimal>::const_iterator it = animals.begin(); it != animals.end(); ++it) {
        (*it)->Accept(amount);
    }

    return 0;
}

diese druckt:

I eat like a generic Animal.
I eat like a wolf!
I eat like a fish!

Per Mikaels Beitrag habe ich einen weiteren Ableger gemacht, der das CRTP verwendet und den Eigenstil von derived() als explizite Unterklassen-Referenz verwendet:

// Adaptation of Visitor Pattern / CRTP from:
// http://stackoverflow.com/a/5872633/170413

#include <iostream>
using std::cout;
using std::endl;

class Base {
public:
  virtual void tpl(int x) = 0;
  virtual void tpl(double x) = 0;
};

// Generics for display
template<typename T>
struct trait {
  static inline const char* name() { return "T"; }
};
template<>
struct trait<int> {
  static inline const char* name() { return "int"; }
};
template<>
struct trait<double> {
  static inline const char* name() { return "double"; }
};

// Use CRTP for dispatch
// Also specify base type to allow for multiple generations
template<typename BaseType, typename DerivedType>
class BaseImpl : public BaseType {
public:
  void tpl(int x) override {
    derived()->tpl_impl(x);
  }
  void tpl(double x) override {
    derived()->tpl_impl(x);
  }
private:
  // Eigen-style
  inline DerivedType* derived() {
    return static_cast<DerivedType*>(this);
  }
  inline const DerivedType* derived() const {
    return static_cast<const DerivedType*>(this);
  }
};

// Have Child extend indirectly from Base
class Child : public BaseImpl<Base, Child> {
protected:
  friend class BaseImpl<Base, Child>;
  template<typename T>
  void tpl_impl(T x) {
    cout << "Child::tpl_impl<" << trait<T>::name() << ">(" << x << ")" << endl;
  }
};

// Have SubChild extend indirectly from Child
class SubChild : public BaseImpl<Child, SubChild> {
protected:
  friend class BaseImpl<Child, SubChild>;
  template<typename T>
  void tpl_impl(T x) {
    cout << "SubChild::tpl_impl<" << trait<T>::name() << ">(" << x << ")" << endl;
  }
};


template<typename BaseType>
void example(BaseType *p) {
  p->tpl(2);
  p->tpl(3.0);
}

int main() {
  Child c;
  SubChild sc;

  // Polymorphism works for Base as base type
  example<Base>(&c);
  example<Base>(&sc);
  // Polymorphism works for Child as base type
  example<Child>(&sc);
  return 0;
}

Ausgabe:

Child::tpl_impl<int>(2)
Child::tpl_impl<double>(3)
SubChild::tpl_impl<int>(2)
SubChild::tpl_impl<double>(3)
SubChild::tpl_impl<int>(2)
SubChild::tpl_impl<double>(3)

Dieser Ausschnitt kann im Quellcode hier gefunden werden: repro: c808ef0: cpp_quick/virtual_template.cc

3
eacousineau

Virtuelle Vorlagenfunktion ist nicht zulässig. Sie können jedoch eine OR die andere hier verwenden.

Sie können eine Schnittstelle mit virtuellen Methoden erstellen und Ihre verschiedenen Tiere in Bezug auf eine Ess-Schnittstelle implementieren. (d. h. PIMPL)

Weniger intuitiv wäre es, wenn eine Nicht-Mitglieder-Nicht-Freund-Vorlage als freie Funktion fungiert, die den Verweis auf jedes Tier aufnehmen und sie entsprechend fressen lässt.

Für die Aufzeichnung brauchen Sie hier keine Vorlagen. Eine rein virtuelle abstrakte Methode für die Basisklasse ist ausreichend, um alle Tiere zu zwingen, wo alle Tiere fressen müssen, und definieren, wie sie dies mit einem Override tun. Ein normales virtuelles Verfahren würde ausreichen, um zu sagen, dass alle Tiere essen können, aber wenn sie keine haben Auf bestimmte Weise können sie diese Standardmethode verwenden.

2
AJG85

Sie können eine Vorlagenklasse mit einer virtuellen Funktion erstellen und die Funktion in der abgeleiteten Klasse implementieren, ohne die Vorlage auf folgende Weise zu verwenden:

a.h:

template <class T>
class A
{
public:
    A() { qDebug() << "a"; }

    virtual A* Func(T _template) { return new A;}
};


b.h:

class B : public A<int>
{
public:
    B();
    virtual A* Func(int _template) { return new B;}
};


and the function CTOR and call 

  A<int>* a1=new B;
    int x=1;
    a1->Func(x);

leider habe ich keine Möglichkeit gefunden, eine virtuelle Funktion mit Vorlagenparametern zu erstellen, ohne die Klasse als Vorlage und ihren Vorlagentyp für die dervied-Klasse zu deklarieren

2
user3103989

Ich habe Ihren Code kopiert und geändert, sodass er jetzt genau so funktionieren sollte, wie Sie möchten:

        #include <iostream>
        #include <vector>

        //defined new enum type
        enum AnimalEnum
        {
           animal,
           wolf,
           fish,
           goldfish,
           other
        };

        //forward declarations
        class Wolf;
        class Fish;
        class GoldFish;
        class OtherAnimal;

        class Animal {
            private:
            AnimalEnum who_really_am_I;
            void* animal_ptr;
            public:
                //declared new constructors overloads for each type of animal
                Animal(const Animal&);
                Animal(const Wolf&);
                Animal(const Fish&);
                Animal(const GoldFish&);
                Animal(const OtherAnimal&);
                template< class AMOUNT >
                /*removed the virtual keyword*/ void eat( AMOUNT amount ) const { 
                    switch (this->who_really_am_I)
                    {
                       case AnimalEnum::other: //You defined OtherAnimal so that it doesn't override the eat action, so it will uses it's Animal's eat
                       case AnimalEnum::animal: std::cout << "I eat like a generic Animal." << std::endl; break;
                       case AnimalEnum::wolf: ((Wolf*)this->animal_ptr)->eat(amount); break;
                       case AnimalEnum::fish: ((Fish*)this->animal_ptr)->eat(amount); break;
                       case AnimalEnum::goldfish: ((GoldFish*)this->animal_ptr)->eat(amount) break;
                    }
                }
                void DeleteMemory() { delete this->animal_ptr; }
                virtual ~Animal() { 
                   //there you can choose if whether or not to delete "animal_ptr" here if you want or not
                }
        };

        class Wolf : public Animal {
            public:
                template< class AMOUNT >
                void eat( AMOUNT amount) const { 
                    std::cout << "I eat like a wolf!" << std::endl; 
                }
                virtual ~Wolf() { 
                }
        };

        class Fish : public Animal {
            public:
                template< class AMOUNT >
                void eat( AMOUNT amount) const { 
                    std::cout << "I eat like a fish!" << std::endl; 
                }
                virtual ~Fish() { 
                }
        };

        class GoldFish : public Fish {
            public:
                template< class AMOUNT >
                void eat( AMOUNT amount) const { 
                    std::cout << "I eat like a goldfish!" << std::endl; 
                }
                virtual ~GoldFish() { 
                }
        };

        class OtherAnimal : public Animal {
                //OtherAnimal constructors must be defined here as Animal's constructors
                OtherAnimal(const Animal& a) : Animal(a) {}
                OtherAnimal(const Wolf& w) : Animal(w) {}
                OtherAnimal(const Fish& f) : Animal(f) {}
                OtherAnimal(const GoldFish& g) : Animal(g) {}
                OtherAnimal(const OtherAnimal& o) : Animal(o) {}
                virtual ~OtherAnimal() { 
                }
        };
        //OtherAnimal will be useful only if it has it's own actions and members, because if not, typedef Animal OtherAnimal or using OtherAnimal = Animal can be used, and it can be removed from above declarations and below definitions

//Here are the definitions of Animal constructors that were declared above/before:    
        Animal::Animal(const Animal& a) : who_really_am_I(AnimalEnum::animal), animal_ptr(nullptr) {}

        Animal::Animal(const Wolf& w) : who_really_am_I(AnimalEnum::wolf), animal_ptr(new Wolf(w)) {}

        Animal::Animal(const Fish& f) : who_really_am_I(AnimalEnum::fish), animal_ptr(new Fish(f)) {}

        Animal::Animal(const GoldFish& g) : who_really_am_I(AnimalEnum::goldfish), animal_ptr(new GoldFish(g)) {}

        Animal::Animal(const OtherAnimal& o) :
    who_really_am_I(AnimalEnum::other), animal_ptr(new OtherAnimal(o)) {}

        int main() {
            std::vector<Animal> animals;
            animals.Push_back(Animal());
            animals.Push_back(Wolf()); //Wolf is converted to Animal via constructor
            animals.Push_back(Fish()); //Fish is converted to Animal via constructor
            animals.Push_back(GoldFish()); //GoldFish is converted to Animal via constructor
            animals.Push_back(OtherAnimal()); //OtherAnimal is converted to Animal via constructor

            for (std::vector<Animal>::const_iterator it = animals.begin(); it != animals.end(); ++it) {
                it->eat(); //this is Animal's eat that invokes other animals eat
                //delete *it; Now it should be:
                it->DeleteMemory();
            }
            animals.clear(); //All animals have been killed, and we don't want full vector of dead animals.

            return 0;
        }

In Ihrem Szenario versuchen Sie, den Kompilierzeitpolymorphismus mit dem Laufzeitpolymorphismus zu mischen, dies kann jedoch nicht in diese "Richtung" erfolgen.

Ihr AMOUNT-Vorlagenargument stellt eine erwartete Schnittstelle für den zu implementierenden Typ dar, basierend auf der Vereinigung aller Operationen, die jede Implementierung von eat verwendet. Wenn Sie einen abstrakten Typ erstellen, der jede dieser Operationen deklariert und sie dort virtuell macht, wo dies erforderlich ist, können Sie eat mit verschiedenen Typen aufrufen (die von Ihrer AMOUNT-Schnittstelle stammen). Und es würde sich wie erwartet verhalten.

0
nate