Generische Programmierung: Instanzen des übergebenen verwalteten Typparameters erzeugen lassen



  • Hallo an alle,

    wie läßt es sich in C++/CLI bewerkstelligen, daß sich eine generische Klasse Instanzen des übergebenen verwalteten Typparameters selbst erzeugt?

    Der Hintergrund: Ich brauche eine Liste, die eine Anzahl Instanzen einer Klasse oder deren Nachfahren enthalten kann. Der genaue Typ der zu speichernden Klassen steht zur Entwurfszeit fest, deshalb möchte ich diesen auch gleich im Code festlegen.

    Beispiel:

    // Eine Listenelement-Klasse
    public ref class CElem
    {
    public: CElem () { };
    };
    
    // Beispiel für davon abgeleitete Klasse
    public ref class CElemChild : CElem { };
    

    Die Liste soll dann wie üblich verwendet werden:

    CElemList <CElem^>      list1 = gcnew CElemList <CElem^> ();       // Liste von CElem-Instanzen
    CElemList <CElemChild^> list2 = gcnew CElemList <CElemChild^> ();  // Liste von CElemChild-Instanzen
    

    Nun möchte ich aber, daß die Liste ihre Elemente selbst erzeugt - und das ist das große Problem:

    template <typename T>
    public ref class CElemList : System::Collections::Generic::List <T>
    {
    public:
    
        CElemList ()
        {
            for (int i = 0; i < 10; i++)   // Die Liste soll selbst Elemente des
                                           // übergebenen Tys T erzeugen
            {
                T foo = gcnew T();   // *** error C3227: "T": "gcnew" kann nicht 
                                     // für die Zuordnung eines generischen Typs
                                     // verwendet werden.
                Add (foo);
            }
        }
    };
    

    Der Aufruf

    gcnew T();
    

    klappt also erwartungsgemäß nicht, da "T" ja "CElem^" (also einschließlich des "^") symbolisiert. Die nun naheliegende Idee, also das T nur für CElem stehen zu lassen, geht aber ebensowenig:

    template <typename T>
    public ref class CElemList : System::Collections::Generic::List <T^>
    // *** error C3225: Das generische Typargument für "T" kann nicht "T ^" sein, es
    // muss ein Werttyp oder ein Handle für einen Verweistyp sein
    

    oder (mit generischer Klasse statt Template):

    generic <typename T>
    where T : CElem
    public ref class CElemList : System::Collections::Generic::List <T^>
    // *** error C3229: "T ^": Dereferenzierungen für einen generischen Typparameter
    // sind nicht zulässig.
    

    Gibt es hierfür eine Lösung? Natürlich kann man die Liste auch nicht-generisch nur für den Basistyp CElem erstellen und dann bei jedem Zugriff wild herumcasten, aber das kann es doch nicht wirklich sein...?

    Vielen Dank schonmal, Lutex



  • Laut einem meiner Bücher (Gordon Hogenson "Foundations of C++/CLI") geht das so

    generic <typename T> where T: gcnew()
    T CreateInstance()
    {
      return gcnew T();
    }
    

    er schreibt dazu

    The constraint is used if you need to use gcnew on the type parameter in the definition of the generic type. The use of gcnew on an unknown type is limited to the default constructor with no arguments.

    Ein anderer Ansatz könnte sein Beispiel mit der Liste, die ListNode structs enthält, die wiederum den eigentlichen Inhalt enthalten, sein.



  • Dass deine generische Klasse eigenständig Elemente des Typparameters erzeugt, wird in der Form nicht gehen, denn es ist ja z. B. gar nicht klar, ob der Typ ein Referenztyp sein wird, geschweige denn ob er einen Standard-Konstruktor haben wird.

    Um dein konkretes Problem zu lösen, müsstest du deiner generischen Klasse eine Form von Factory mitgeben, welche weiß, wie man die Objekte erzeugt.

    In C# (⚠) würde ich das so machen:

    class MyGeneric<T>
    {
    	T[] array;
    
    	public MyGeneric(Func<T> CreateT)
    	{
    		array = new T[10];
    
    		for (int i = 0; i < array.Length; ++i)
    			array[i] = CreateT();
    	}
    
    	public void Print()
    	{
    		foreach (T t in array)
    			Console.WriteLine(t.ToString());
    	}
    }
    
    class Foo
    {
    	public override string ToString()
    	{
    		return "Foo";
    	}
    }
    
    class Bar
    {
    	public override string ToString()
    	{
    		return "Bar";
    	}
    }
    
    class Program
    {
    	static void Main(string[] args)
    	{
    		MyGeneric<Foo> fooList = new MyGeneric<Foo>(() => new Foo());
    		MyGeneric<Bar> barList = new MyGeneric<Bar>(() => new Bar());
    
    		fooList.Print();
    		barList.Print();
    	}
    }
    

    Hier wird beim Erstellen eines Objekts der generischen Klasse ein Delegate (vgl. Funktionszeiger in C++) übergeben, welcher Objekte vom enthaltenen Typ erzeugt. Dank C#-Lambda-Expressions ist das ganze schön kurz formuliert. Wie man das in C++/CLI machen würde, kann ich nicht sagen, da ich mich damit nicht wirklich auskenne.

    Dein Problem deutet allerdings darauf hin, dass das Design deiner Liste nicht optimal ist. Normalerweise erzeugt eine Liste ihre Elemente nicht selbst.



  • Den von nn genannten Constraint kannte ich noch nicht. Der ist natürlich besser.

    Mein Ansatz könnte aber dann noch interessant sein, wenn die Klasse keinen Standard-Konstruktor hat.



  • Vielen Dank für die schnelle Hilfe!

    Unter der Voraussetzung, dass von der Listenelement-Klasse der Standardkonstruktor benutzt werden soll (trifft bei mir zu), hab ich nun doch noch eine Lösung im MSDN gefunden, allerdings gut versteckt:

    // T foo = gcnew T();   wird ersetzt durch
    T foo = safe_cast <T> (System::Activator::CreateInstance (T::typeid));
    

    Das funktioniert prächtig!



  • Ich würde den ANsatz von nn wählen, da dieser zur Compilezeit geprüft werden kann, ob dies auch möglich ist. Deine Lösung würde erst zur Laufzeit eine Exception auslösen, was sehr unschön ist...



  • Allerdings hat Activator::CreateInstance auch Überladungen für Konstruktoren mit Parametern, wenn man das braucht, führt wohl kein Weg daran vorbei.



  • Trotzdem ist eine Compilezeit-Prüfung immer einer Laufzeit-Prüfung vorzuziehen!


Anmelden zum Antworten