Arbeiten mit .NET: Materialien/ C++ CLI templates zu .NET-Komponenten machen
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.
C# generics vs. C++ Templates
BearbeitenIm 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>
{
// ....
};
Implementierung einer C++/CLI Template-Klasse mit C# Generic Interface
BearbeitenDamit 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_;
}
}
};
.NET Factory für C++/CLI templates
BearbeitenDa 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!";
Automatisches Mapping von Parametern zwischen Generics und Templates
BearbeitenDer 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
gcroot
einoperator<
keinen Sinn macht und deshalb nicht definiert ist, eine automtische Übersetzung desSystem::String ^
in einenstd::string
. Dies wird über Marshalling mittelsmarshal_as
erreicht.
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);
}
}
};