.NET/ C++/CLI templates via C-Sharp generic interfaces zu .NET-Komponenten machen
Aus Wikibooks
C++/CLI ist eine von Microsoft entwickelte Variante der Programmiersprache C++, die den Zugriff auf die virtuelle Laufzeitumgebung der .NET-Plattform mit Hilfe von speziell darauf zugeschnittenen Spracherweiterungen ermöglicht. Umgekehrt ist es mit entsprechendem Aufwand auch möglich, von einer .NET-Sprache wie z.B. C# aus auch auf Sprachfeatures zuzugreifen, die sonst nur in der C++-Welt vorhanden sind. Dieser Artikel behandelt die Frage, wie über C# generics auf C++ Templates zugegriffen werden kann und damit die Vorteile von C++ auch in C# nutzbar sind.
Inhaltsverzeichnis |
[Bearbeiten] C# generics vs. C++ Templates
Im Gegensatz zur wohl durchdachten, besonders sorgfätig entwickelten Unterstützung für generische Programmiertechniken in C++, ist eine solche in C# nur rudimentär vorhanden. Selbst besonders wichtige Techniken wie Spezialisierung und partielle Spezialisierung werden nicht unterstützt. Dadurch sind C# generics für anspruchsvollere Programmieraufgaben praktisch wertlos. Trotzdem ist es dank C++/CLI möglich, C++ Templates für C# zugänglich zu machen.
C# kennt als Sprachelement neben generischen Klassen auch generische Interface-Definitionen:
public interface IMatrix<T> { int Size1 { get; } int Size2 { get; } T this[int IndexI, int IndexJ] { get; set; } }
Die selbe Interface-Definition kann auch in C++/CLI angegeben werden:
// compile with: /clr generic<typename T> public interface class IMatrix { property int Size1{ int get(); } property int Size2{ int get(); } property T ^default[int, int] { T ^get(int, int); void set(int, int, T^); } };
Da in C++/CLI Template-Klassen von .NET Interfaces erben können, ist es also im Prinzip möglich, die Parameter des generischen Interface auf die Template-Parameter zu mappen. Die besondere Schwierigkeit hierbei ist die Tatsache, dass C# Generics erst zur Laufzeit instanziiert werden, C++ Templates aber schon zur Compile-Zeit.
Dadurch ist es nicht möglich, den Typ-Parameter einer Generic-Klasse als Argument für ein C++ Template zu verwenden. Der folgende C++/CLI-Code ist also nicht gültig:
// compile with: /clr template <typename T> class AClassicalTemplate {}; generic <typename T> public ref class Wrong { AClassicalTemplate<T> * Ouch; };
Umgekehrt ist es natürlich möglich, C++ Template-Parameter als Parameter für C# Generics zu verwenden:
generic <typename T> public ref class AVerySimpleGeneric { // ... }; template <typename T> ref class Right { AVerySimpleGeneric<T> ^ Yeah; };
Ebenfalls möglich: eine C++/CLI Template-Klasse erbt von einem C# Generic Interface.
// compile with: /clr template <typename T> ref class Matrix : public IMatrix<T> { // .... };
[Bearbeiten] Implementierung einer C++/CLI Template-Klasse mit C# Generic Interface
Damit ist es also möglich, eine Template-Klasse in C++/CLI so zu implementieren, dass sie das C# Generic Interface unterstützt. Hier eine (verbesserungswürdige) Version einer Matrix.
#include <vector> // compile with: /clr template <typename T> ref class Matrix : public IMatrix<T> { std::vector<std::vector<T> > * m_Data; int i_, j_; public: Matrix(int i, int j) : i_(i) , j_(j) , m_Data(new std::vector<std::vector<T> >(i)) { std::vector<T> v(j); for (int ii = 0; ii < i; ++ii) { (*m_Data)[ii] = v; } } property T default[int, int] { virtual T get(int i, int j) { return (*m_Data)[i][j]; } virtual void set(int i, int j, T d) { (*m_Data)[i][j] = d; } } property int Size1 { virtual int get() { return i_; } } property int Size2 { virtual int get() { return j_; } } };
Diese Klasse lässt sich nun leider nur mit nativen C++-Typen parametrisieren. Will man in C++/CLI .NET-Datentypen verwenden, so geht dies nur über eine Kapselung in die Klasse gcroot. Das soeben verwendete Template kann man für in gcroot gekapselte Klassen spezialisieren:
template <typename T> ref class Matrix<gcroot<T> > : public IMatrix<T> { std::vector<std::vector<gcroot<T> > > * m_Data; int i_, j_; public: Matrix(int i, int j) : i_(i) , j_(j) , m_Data(new std::vector<std::vector<gcroot<T> > >(i)) { std::vector<gcroot<T> > v(j); for (int ii = 0; ii < i; ++ii) { (*m_Data)[ii] = v; } } property T default[int, int] { virtual T get(int i, int j) { return (*m_Data)[i][j]; } virtual void set(int i, int j, T d) { (*m_Data)[i][j] = d; } } property int Size1 { virtual int get() { return i_; } } property int Size2 { virtual int get() { return j_; } } };
Das ist natürlich alles andere als elegant, weil man jede Menge Code dupliziert. Die Einführung einer kleinen traits-Klasse erleichtert die Benutzung sowohl für native C++-Typen als auch für .NET Typen, die gekapselt werden müssen:
template <typename T> struct gc_type_dispatcher { typedef T type; }; template <typename T> struct gc_type_dispatcher<gcroot<T> > { typedef T type; }; template <typename T> ref class Matrix : public IMatrix<typename gc_type_dispatcher<T>::type> { std::vector<std::vector<T> > * m_Data; int i_, j_; public: Matrix(int i, int j) : i_(i) , j_(j) , m_Data(new std::vector<std::vector<T> >(i)) { std::vector<T> v(j); for (int ii = 0; ii < i; ++ii) { (*m_Data)[ii] = v; } } typedef typename gc_type_dispatcher<T>::type contained_t; property contained_t default[int, int] { virtual contained_t get(int i, int j) { return (*m_Data)[i][j]; } virtual void set(int i, int j, contained_t d) { (*m_Data)[i][j] = d; } } property int Size1 { virtual int get() { return i_; } } property int Size2 { virtual int get() { return j_; } } };
[Bearbeiten] .NET Factory für C++/CLI templates
Da C++/CLI Template Klassen außerhalb von C++/CLI nicht sichtbar sind, können wir die Klasse Matrix von 'C# aus nicht direkt benutzen. Hier hilft das sog. Factory Pattern weiter: Wir schreiben eine Klasse mit Methoden, die Objekte von Typ der Template Klasse Matrix<...> auf dem managed heap erzeugen und einen Managed Pointer auf das Interface zurückgeben.
public ref class MatrixFactory { public: IMatrix<double> ^ CreateMatrixDouble(int i, int j) { return gcnew Matrix<double>(i, j); } IMatrix<int> ^ CreateMatrixInt(int i, int j) { return gcnew Matrix<int>(i, j); } IMatrix<System::String ^> ^ CreateMatrixString(int i, int j) { return gcnew Matrix<gcroot<System::String^> >(i, j); } };
Nun kann man von C# aus Objekte vom Typ Matrix<...> instanziieren:
MatrixFactory p = new MatrixFactory(); IMatrix<Double> m1 = p.CreateMatrixDouble(3, 4); IMatrix<int> m2 = p.CreateMatrixInt(4, 5); IMatrix<String> m3 = p.CreateMatrixString(3, 4); // ... m1[0, 1] = 42.0; m2[1, 2] = 42; m3[2, 3] = "Yeah!";
[Bearbeiten] Automatisches Mapping von Parametern zwischen Generics und Templates
Der bisher angewendete Ansatz ist insofern noch nicht perfekt, als das Mapping zwischen den Parametern der Generic Interfaces und den Factory-Methoden von Hand geschieht. Bei einer großen Anzahl von Factory-Methoden führt das zu einem sog. fetten Interface, das sich auch noch bei neu zu unterstützenden Datentypen immer wieder ändert.
Aus Benutzersicht und unter Design-Aspekten wäre ein Aufruf in dieser Form hier viel besser:
MatrixFactory p = new MatrixFactory(); IMatrix<Double> m1 = p.CreateMatrix<Double>(3, 4); IMatrix<int> m2 = p.CreateMatrix<int>(4, 5); IMatrix<String> m3 = p.CreateMatrix<String>(3, 4);
Da C# Generics im Gegensatz zu C++ Templates erst zur Laufzeit instanziiert werden, ist ein automatisches Mapping zwischen den Parametern des Generic Interface und den Template-Parametern erst zur Laufzeit möglich. Interessanterweise unterstützt C# Reflection, weshalb die Typinformation sehr detailliert zur Laufzeit zur Verfügung steht. Ein erster Versuch für eine "generische" Factory könnte deshalb so aussehen:
public ref class MatrixFactory { private: IMatrix<double> ^ CreateMatrixDouble(int i, int j) { return gcnew Matrix<double>(i, j); } IMatrix<int> ^ CreateMatrixInt(int i, int j) { return gcnew Matrix<int>(i, j); } IMatrix<System::String ^> ^ CreateMatrixString(int i, int j) { return gcnew Matrix<gcroot<System::String^> >(i, j); } public: generic <typename T> IMatrix<T> ^ CreateMatrix(int i, int j) { if (T::typeid == int::typeid) { return (IMatrix<T> ^)(MatrixCreator<IMatrix<int> >().create(i, j)); } else if (T::typeid == double::typeid) { return (IMatrix<T> ^)(MatrixCreator<IMatrix<double> >().create(i, j)); } else if (T::typeid == System::String::typeid) { return (IMatrix<T> ^)(MatrixCreator<IMatrix<System::String ^> >().create(i, j)); } else { throw gcnew Exception("No CreateMatrix available for " + T::typeid); } } };
Dieser Code hat bereits einige Auffälligkeiten: Interessant ist vor allen Dingen, dass wirklich jeder Datentyp in C++/CLI eine statische Member-Variable ::typeid besitzt. So lässt sich zu Laufzeit der Typ des Parameters auf die vorhandenen Factory-Funktionen mappen. Der Compiler übertreibt dabei allerdings ein wenig bei der Typsicherheit und unterscheidet zwischen IMatrix<T> und IMatrix<int>. Darum muss der Rückgabewert noch einmal auf seinen eigenen Typ gecastet werden.
Der vorgeschlagene Code hat allerdings einen großen Nachteil: Wenn die Anzahl der zu mappenden Factory-Funktionen groß wird, hat man eine große Anzahl an Typvergleichen im if-then-else-Teil der Generischen Factory-Funktion. Das ist u.U. nicht tragbar. Besser wäre es, man könnte die vorhandenen Factory-Funktionen in einer Lookup-Table speichern und abhängig vom Typ abrufen.
Um die Factory-Funtionen in einer Lookup-Table (z.B. std::map) speichern zu können, müssen alle Factory-Funtionen vom gleichen Typ sein, was sie zur Zeit nicht sind. Leider unterstützt C++/CLI keine covarianten Rückgabetypen, so dass man auch nicht alle Rückgabetypen von einer Basisklasse ableiten könnte und über diesen Trick eine einheitliche Signatur hinbekäme. C# (in .NET 3.0) unterstützt covariante Rückgabetypen für Delegates, C++/CLI ist hier leider nicht so weit ausgebaut. Eine Rückgabe als void * scheidet ebenfalls aus, weil man .NET Datentypen nicht auf void * casten kann und umgekehrt.
Glücklicherweise haben alle Typen in .NET System::Object als Basisklasse. So lassen sich alle Factory-Funktionen auf einen Delegate-Typ mappen:
public delegate System::Object ^ MatrixCreationFn(int i, int j); MatrixCreationFn ^ op = gcnew MatrixCreationFn(&MatrixCreator<IMatrix<int> >::create); // usw ...
Wenn wir für die Lookup-Table eine std::map verwenden wollen, so brauchen wir folgende Features, die Microsoft für die Unterstützung der Interoperabilität zur Verfügung stellt:
- Den Wrapper
gcroot, um die Delegates im nativen Container speichern zu können - Eine automatische Übersetzung des Typs in einen String, der als Lookup-Key verwendet werden kann. Hierzu gibt es
X::typeid->ToString(). - Weil für
gcrooteinoperator<keinen Sinn macht und deshalb nicht definiert ist, eine automtische Übersetzung desSystem::String ^in einenstd::string. Dies wird über Marshalling mittelsmarshal_aserreicht.
Ausgerüstet mit diesen Mitteln gelingt die Implementierung incl. Table-Lookup auf folgende Weise:
#include <map> #include <msclr\marshal_cppstd.h> template <typename T> ref class MatrixCreator; template <> ref class MatrixCreator<IMatrix<double> > { public: static System::Object ^ create(int i, int j) { return gcnew TestMatrix<double>(i, j); } }; template <> ref class MatrixCreator<IMatrix<int> > { public: static System::Object ^ create(int i, int j) { return gcnew TestMatrix<int>(i, j); } }; template <> ref class MatrixCreator<IMatrix<System::String ^> > { public: static System::Object ^ create(int i, int j) { return gcnew TestMatrix<gcroot<System::String ^> >(i, j); } }; public delegate System::Object ^ MatrixCreationFn(int i, int j); public ref class MatrixFactory { private: typedef std::map<std::string, gcroot<MatrixCreationFn ^> > factory_storage_t; factory_storage_t * m_pFactoryFunctions; void InitializeFactoryFunctions() { m_pFactoryFunctions->insert(std::make_pair (marshal_as<std::string>(int::typeid->ToString()), gcroot<MatrixCreationFn ^> (gcnew MatrixCreationFn(&MatrixCreator<IMatrix<int> >::create)))); m_pFactoryFunctions->insert(std::make_pair (marshal_as<std::string>(double::typeid->ToString()), gcroot<MatrixCreationFn ^> (gcnew MatrixCreationFn(&MatrixCreator<IMatrix<double> >::create)))); m_pFactoryFunctions->insert(std::make_pair (marshal_as<std::string>(System::String::typeid->ToString()), gcroot<MatrixCreationFn ^> (gcnew MatrixCreationFn(&MatrixCreator<IMatrix<System::String ^> >::create)))); } public: MatrixFactory() : m_pFactoryFunctions(new factory_storage_t()) { InitializeFactoryFunctions(); } public: generic <typename T> IMatrix<T> ^ CreateMatrix(int i, int j) { std::string type_id_as_string = marshal_as<std::string>(T::typeid->ToString()); factory_storage_t::const_iterator lb = m_pFactoryFunctions->lower_bound(type_id_as_string); if(lb != m_pFactoryFunctions->end() && !(m_pFactoryFunctions->key_comp()(type_id_as_string, lb->first))) { MatrixCreationFn ^ fn = lb->second; return (IMatrix<T> ^)(fn(i, j)); } else { throw gcnew Exception("No CreateMatrix available for " + T::typeid); } } };