Dědičnost a virtuální metody
Managed objekty bohužel nepodporují vícenásobnou dědičnost, může dědit maximálně z jedné ref třídy a neomezeného množství interfaců. Celkem logicky nemůže třída dědit z hodnotových či unmanaged typů.
S dědičností úzce souvisí také možnost definovat a používat virtuální a abstraktní metody. Virtuální metody se deklarují klíčovým slovem virtual před metodu. Pokud ji chceme overridnout, tak je třeba ji označit jako virtual před overridující metodu a ještě za seznam parametrů napsat override. Abstraktní (čistě virtuální) funkci lze deklarovat 2 způsoby: buď klasickým C++ způsobem (virtual void Neco() = 0;) nebo nově pomocí klíčového slova abstract psaného za seznam parametrů (tj. místo “=0”). Abstraktní metody můžeme psát pouze do abstraktních tříd, čímž dáváme najevo, že nebudeme (ani nemůžeme) přímo tvořit její instance. Abstraktní třídou se třída stane automaticky, pokud je v ní aspoň 1 abstraktní metoda, nicméně Visual Studio pak zobrazí warning – proto doporučuji nejen z tohoto důvodu, ale hlavně pro přehlednost, deklarovat abstraktní metody explicitne pomocí slova abstract psaného za název třídy.
public ref class Bazova abstract // klíčové slovo abstract není povinné, ale bez něj to není tak přehledné a navíc dostáváme warning
{
protected:
virtual void Fce() abstract; //definice abstraktní metody
//virtual void Fce() = 0; // 2. způsob def. abstr. metody ("jako C++")
virtual void VirtFce() { /* ... */ } //definice virtuální metody
};
public ref class Trida : public Bazova
{
protected:
virtual void Fce() override { std::cout << "Ahoj"; } //overridnutí abstraktní metody
virtual void VirtFce() override // overridnutí virtuální metody
{
Bazova::VirtFce(); // voláme VirtFce z předka
Console::WriteLine("ahoj z virt fce");
}
};
Viditelnost tříd a členských proměnných
Viditelnost se v C++/CLI píše stejně jako v nativním C++, tedy nepíše se před každou metodu či proměnnou, ale napíše se jen <viditelnost>: a všechno až do další změny má pak danou viditelnost. U unmanaged tříd se nic nemění, managed třídy podporují následující viditelnosti svých členských proměnných, funkcí či properties:
Viditelnost |
Popis |
Ekvivalent C# |
Ekvivalent Vb.Net |
public |
Viditelné odevšad |
public |
Public |
private |
Viditelné jen z kontextu aktuální třídy |
private |
Private |
protected |
Viditelné z aktuální třídy a jejích potomků |
protected |
Protected |
internal |
Viditelné z aktuální assembly (je public pro aktuální assemly a private jinak) |
internal |
Friend |
public protected
protected public |
Public pro aktuální assembly, protected pro jiné assembly |
protected internal |
Protected Friend |
private protected
protected private |
Protected pro aktuální assembly,
private pro jiné |
není |
není |
Třídy pak mohou být buď public (mají být vidět z jiných assembly) nebo private(nemají být vidět z jiných assemly). Defaultní je private. Zapisuje se to před
ref class či
value class.
public ref class Trida
{
public:
static int X;
protected private:
int PP;
};
Properties
Properties jsou věc, která se ve standardním C++ vůbec nevyskytuje a tak je bylo třeba udělat “from scratch”. Mimochodem, pokud byste chtěli používat properties ve Visual C++, tak tam to možné je díky jednomu parametru nestandardního rozšíření __descspec – viz http://msdn.microsoft.com/en-us/library/yhfk0thd.aspx. Ale zpět k managed kódu. Properties se deklarují pomocí syntaxe připomínající syntaxi z C#, akorát je lehce upovídanější - místo deklarace getteru a setteru pomocí get a set se nadeklarují jakési "submetody" (nenapadá mě vhodnější výraz) get a set.
Příklad triviální property, která pouze čte či nastavuje proměnnou x:
ref class Xsi {
private:
int x;
public:
property int MojeProperty // deklarace property
{
void set(int v) { x = v ; } //setter
int get() { return x; } //getter
}
};
Funkce get() a set(int) jsou getter a setter pro danou property, přičemž platí, že getter se musí jmenovat get a vracet typ property a setter se jmenuje set a pro změnu má jeden parametr typu stejného jako je typ property. Stejně jako ve VbNetu mohou být property virtuální nebo statické (modifikátory static či virtual se píši před klíčové slovo property, tj např. virtual property int MojeProperty). Kódu se pak používají stejně jako normální proměnná, tj. nastavují se např. x.MojeProperty=5; a čtou cout << x.MojeProperty;
C++/CLI podporuje i indexované property (včetně multidimenziálních). Typy indexu (či indexů) se pak píší do hrananých závorek za jméno property. Getteru i setteru pak přibudou parametry s indexem (indexy), u setteru se parametr s novou hodnotou property tak posune na poslední místo. Drobná variace předchozího příkladu, kde proměnná x se nám změnila v dvoudimenzionální pole:
ref class Xsi {
private:
array<int, 2> ^x;
public:
property int MojeProperty[int, int] // deklarace property
{
void set(int i, int j, int v) { x[i,j] = v; } //setter, i a j je index, v je hodnota
int get(int i, int j) { return x[i,j]; } //getter, i a j je index
}
};
gcroot – vkládáme managed objekty do nativních tříd
Jak jsem psal v úvodním povídání o managed a unmanaged objektech, není možné přímo managed proměnné v nativních třídách. Nicméně existuje kouzelná template třída gcroot<>, která může obsahovat managed objekt, přitom je sama unmanaged. Její použití je velmi jednoduché, viz příklad:
ref class Xsi
{
public:
int A;
};
class NativniTrida
{
public:
gcroot<Xsi ^> xsi; //přestože Xsi je managed třída, tak díky gcrootu ji můžeme použít i v C++ třídě
void NastavA(int a) { xsi->A=a; }
};
Stack semantics pro referenční objekty
Pokud jste přemýšleli, co se stane, když nenadefinuju referenční objekt jako handle (Typ^), ale stejně jako definujeme hodnotové typy (tj. pomocí tzv. stack semantics, podle toho, že hodnotové typy sedí na stacku), tak se můžete radovat, neboť v následujících řádcích vám toto tajemství bude odhaleno.
Pokud tedy nadeklarujete naší oblíbenou (a pořád se měnící) třídu Xsi jako Xsi x; tak kompilátor interně vytvoří instanci třídy Xsi na managed haldě (tj jako kdybyste použili gcnew). Nicméně práce s ním se celkem liší a částečně tak připomíná práci s klasickými hodnotovými typy. Předně k přístupu k členským prvkům nepoužíváte –> jako u handles ale . jako u struktur. Za druhé, pokud objekt přiřadíte do jiné proměnné (tj mám Xsi a; Xsi b; a udělám a = b;), tak se defaultně udělá mělká (shallow) kopie objektu a(z našeho příkladu) a ta bude v proměnné b. Tedy a i b budou odkazovat na různé objekty, pouze b bude mělkou kopíí a. Pokud chceme objekty kopírovat nějak chytřeji než pomocí mělké kopie, musíme si přetížit operátor = (přiřazovací operátor). Reálně se tedy nový objekt nevytváří, ale nakopíruje se jeden objekt do druhého. Další zvláštnost je, že narozdíl od objektů vytvářených pomocí gcnew, mají tyto objekty definovanou dobu života, tedy pokud program vyleze k bloku, ve kterém byl objekt definován, tak se automaticky zavolá destruktor objektu (pro připomenutí: destruktor managed objektu v C++/CLI odpovídá metodě Dispose). Pokud napíšete funkci, které má jako jeden z parametrů objekt ne jako handle, tak se té funkci předá kopie objektu vytvořená pomocí kopírovacího konstruktoru. Neboli stručně řečeno – přestože se objekt interně vytváří na managed haldě, tak se na pohled chovají do značné míry jako kdyby byli na stacku.
ref class Xsi
{
public:
int A;
Xsi ^operator = (Xsi %a) // je třeba a nadeklarovat jako tracking reference, jinak by to nedávalo smysl
{
A = a.A;
return this;
}
};
int main(array<System::String ^> ^args)
{
Xsi a, b; // definujeme referenční objekty Xsi jako kdyby byly na stacku
a.A=5;
b = a; // kopie a do b
a.A=6;
Console::WriteLine("{0} {1}", a.A, b.A); // vypíše 6 5
}
Delegates and events
Předpokládám, že co je delegát a event (událost) dobře znáte, pokud ne, tak si to můžete osvěžit v nějakém článku na tomto serveru pojednávajícím o této problematice. Takže se můžeme vrhnout na to, jak se s tím pracuje v C++/CLI.
Delegát
Delegáty se deklarují v podstatě úplně stejně jako v C# a zachází se s nimi velmi podobně (se stejnou myšlenkou, jen syntakticky převedenou do C++/CLI) – pomocí klíčového slova delegate. Vytváří se pomocí gcnew, konstruktorem majícím jeden parametr – funkci, které bude zavolána při zavolání toho delegátu. Je dobré vědět, že můžete vytvářet pouze delegáty na managed funkce, unmanaged funkce musíte nejdříve zabalit do nějaké managed funkce.
ref class Xsi {
public:
delegate void MujDelegat(int n); // deklarace typu delegátu
static void fce1(int n)
{
Console::WriteLine(n*n);
}
static void fce2(int n)
{
Console::WriteLine(2*n);
}
};
int main(array<System::String ^> ^args)
{
Xsi::MujDelegat ^deleg = gcnew Xsi::MujDelegat(Xsi::fce1); //vytvoření delegátu
deleg+=gcnew Xsi::MujDelegat(Xsi::fce2); // přidání další fce do delegátu
deleg(5); // zavolání všech funkcí v delegátu, vypíše 25 a pak 10
Console::WriteLine("Odebrání fce1");
deleg-=gcnew Xsi::MujDelegat(Xsi::fce1);
deleg(5); // vypíše už jen 10
}
Event
Eventy se deklarují klíčovým slovem event, za kterým nasleduje typ delegátu a název eventu. Volají se stejně jako delefáty či jakéholi jiné funkce. Příklad základního eventu:
ref class Xsi {
public:
delegate void MujDelegat(int n);
static void fce1(int n)
{
Console::WriteLine(n*n);
}
static void fce2(int n)
{
Console::WriteLine(2*n);
}
event MujDelegat^ MojeUdalost;
void StaloSeTo() { MojeUdalost(10); } // zavolání události (firing the event)
};
int main(array<System::String ^> ^args)
{
Xsi ^x = gcnew Xsi();
x->MojeUdalost += gcnew Xsi::MujDelegat(Xsi::fce1); // registrace prvního handleru našeho eventu
x->MojeUdalost += gcnew Xsi::MujDelegat(Xsi::fce2); // registrace druhého handleru našeho eventu
x->StaloSeTo();
}
Pokud bychom chtěli si napsat vlastní akce, které se provedou při přidání/odebrání obsluhy události (handleru) či vyvolání eventu, tak máme tu možnost toto provést pomocí deklarace “submetod” (nevím jak jinak to pojmenovat, viz příklad). Je třeba ale počítat s tím, že tím přepíšeme výchozí akci, takže pokud chceme, aby event byl eventem, tak jak jsme zvyklí, tak si musíme nadeklarovat ještě pomocného delegáta obsahujícího všechny handlery. Máme možnost přepsat zavolání (submetoda
raise), přidání (submetoda
add) a odebrání (submetoda
remove) handleru. Myslím, že nejjasnější bude uvedení komentovaného příkladu:
ref class Xsi {
public:
delegate void MujDelegat(int n);
private:
MujDelegat ^_MojeUdalost; // delegat obsahující handlery na událost MojeUdalost
public:
static void fce1(int n)
{
Console::WriteLine(n*n);
}
static void fce2(int n)
{
Console::WriteLine(2*n);
}
event MujDelegat ^MojeUdalost
{
protected: // chceme, aby událost mohly vyvolat jen metody této třídy či potomků, tak ji nastavíme visibilitu na protected
void raise(int n) { // vlastní akce při vyvolání události
Console::WriteLine("Vyvolana udalost s n = {0}", n);
_MojeUdalost(n); // zavolám všechny handlery pomocí delegátu _MojeUdalost
}
public: // přidávat a odebírat handlery pak může každý
void add(MujDelegat ^handler) { // vlastní akce při přidání nového handleru
Console::WriteLine("Pridan novy handler");
_MojeUdalost+=handler; // přidání handleru do delegátu _MojeUdalost
}
void remove(MujDelegat ^handler) { // vlastní akce při odebrání handleru
Console::WriteLine("odebran handler");
_MojeUdalost-=handler; // odebrání handleru z delegátu
}
}
void StaloSeTo() { MojeUdalost(10); } // zavolání události (firing the event)
};
int main(array<System::String ^> ^args)
{
Xsi ^x = gcnew Xsi();
x->MojeUdalost += gcnew Xsi::MujDelegat(Xsi::fce1); // registrace prvního handleru našeho eventu
x->MojeUdalost += gcnew Xsi::MujDelegat(Xsi::fce2); // registrace druhého handleru našeho eventu
x->StaloSeTo();
}
Tento příklad vypíše:
Pridan novy handler
Pridan novy handler
Vyvolana udalost s n = 10
100
20
Připichování aneb vytváříme pořádné pointery
Přestože C++/CLI se snaží oddělovat managed a unmanaged data, tak někdy je třeba pracovat s managed daty pomocí unmanaged kódu. Příklad můžete najít v převodu managed stringu na char *. Na první pohled by se nemělo jednat o nic těžkého, prostě si udělám pointer někam na managed haldu. Nicméně na druhý pohled už vyvstane problém, kam ten by onen pointer měl ukazovat. Managed objekty samožřejmě nevisí někde ve vakuu, ale sedí na nějaké adrese v paměti. Problém je v tom, že ta adresa nemusí být stálá, Garbage Collector může managed objekty přesouvat po paměti, jak potřebuje. U unmanaged objektů samozřejmě takový problém není, tam s nimi nikdo nešoupe (většinou). C++/CLI nám nabízí 2 řešení tohoto problému: první spočívá v tom, že si managed objekt připíchnu (nebo pro lepší představu přitluču hřebíkem, nicméně narozdíl od "fyzického” světa, kde přitlučené objekty či zvířata jsou tím poškozena, tak naše managed objekty to nijak nepoškozuje:) ). Celé se to točí okoli template třídy pinned_ptr<Typ>, která má podobný význam jako konstrukce fixed v C# – místo klasického pointeru totiž uděláme pinned_ptr, čímž dáme GC najevo, že s objektem nemá hýbat. Příklad by měl vše náležitě osvětlit:
ref class Xsi {
public:
int A, B, C;
};
int main(array<System::String ^> ^args)
{
Xsi ^myobj = gcnew Xsi();
myobj->A=123; // nastavím myobj->A
int *ptr = & myobj->A; // tento pokus na unmanaged způsob nám kompilátor nepovolí
pin_ptr<int> ptr2 = &myobj->A; // připíchnu si myobj, takže se nebude přesouvat po paměti a unmanaged pointer na něj bude platný
*ptr2 = 321; // změním hodnotu na adrese ptr2 (který míří na myobj->A)
Console::WriteLine(myobj->A); // test, zda-li je vše OK - vypíše se 321
}
Druhou možností, jak rýpat doprostřed managed objektů je interior_ptr<> – syntakticky se používá stejně jako pinned_ptr, nicméně narozdíl od něj nepřipichuje objekty ale volí jiný postup – updatuje se podle toho, jak GC přesunuje objekty po paměti, tak, aby pořád ukazovaly na tu správnou proměnnou ve správném objektu.
Jak interior_ptr tak pinned_ptr jsou nadmnožinou klasických unmanaged pointerů, takže do nic můžete dávat i adresy na unmanaged paměť (kde se samozřejmě ony vylepšení nepoužijí).
Tracking references
Tracking references jsou v podstatě managed obdobou references (&) známých z C++ – umožňují předávat parametry funkci referencí (obdoba ref v C#), používat je jako not-null obdobu pointeru, procházet kolekci for eachem a rovnou prvky kolekce měnit (viz příklad v minulém díle, v odstavci o for eachi) a podobně. Narozdíl od normálních referencí jim dali vývojáři do vínku kompatibilitu s GC a filozofií managed objektů (stejně jako interior_ptr se updatují tak, aby pořád ukazovaly na objekt). Narozdíl od handles (^), které mohou odkazovat pouze na celý managed objekt, tyto mohou se ukazovat i na proměnné nějakého objektu. A narozdíl od interior_ptr nemohou být null. Triviální příklad předávání referencí u funkce:
void funkce(String ^%str)
{
str = "ahoj";
}
int main(array<System::String ^> ^args)
{
String ^s = "baf";
funkce(s);
Console::WriteLine(s); // vypíše "ahoj". Pokud by funkce byla definovaná jako void funkce(String ^str), tak by v s bylo pořád "baf"
}
Šablony a generika
Jako poslední věc z C++/CLI si popíšeme šablony a generika. V C++/CLI jsou oba pojmy striktně rozděleny – šablony (templates) jsou uvedeny klíčovým slovem template a mají stejné možnosti jako mají nativním C++, přičemž jdou aplikovat i na managed typy. Generika oproti tomu odpovídají generickým typům, tak jak je známe z čistých .NET jazyků. Jsou si v mnohém podobné, obojí zajišťují to, aby jsme nemuseli mít 100 tříd dělajících to samé, ale každou specializovanou na jiný typ. Toho se dosahuje tak, že třída má tzv. typový parametr, který můžete chápat jako neznámý typ – stejně jako např. parametry funkcí jsou neznámou konstantou. Typový parametr má samozřejmě nějaké jméno, například T. Pokud pak deklarujete objekt daného typu, tak za jméno typu připojíte konkrétní hodnotu toho typového parametru v < > (tedy napíšu např. List<int>, kde List je generická třída a int je pak konkrétní hodnota typového parametru) a kompilátor (u šablon) nebo .NET runtime (u generik) potom namísto onoho T, které jsme použili při psaní generické třídy, dosadí konkrétní typ (v našem případě int). Více si o generickém programování můžete přečíst třeba na Wikipedii. A my se můžeme pustit do vysvětlování, jak se to dělá v C++/CLI.
Přestože na první pohled jsou generika a šablony velmi podobné (a občas se i zaměňují, což většinou nevadí, neboď jazyk typicky podporuje jen jedno z nich, C++/CLI však umí oboje), tak z hlediska kompilátoru to jsou naprosto odlišné věci. Pokud znáte dobře C++ templates, tak jistě víte, že na rozdíl od .NET generik jsou vyhodnocovány při kompilaci, což platí i pro managed třídy, z čehož vyplývá velké množství rozdílů, které se při bližsším pohledu objeví. Nejlepší bude asi uvést přehlednou tabulku s rozdíly mezi oběma konstrukty:
|
Generika |
Šablony |
Klíčové slovo |
generic |
template |
Vyhodnocování |
V runtimu (při JIT kompilaci) |
Už při kompilaci se z nich dělají normální nešablonové negenerické typy |
Kompatibilita s C#, VB.NET |
Ano |
Ne |
Použití netypových parametrů (template <char x>) |
Ne |
Ano |
Použití v jiných assembly |
Bezproblémové |
Mohou nastat problémy, díky specializaci při kompilaci, volající assembly si je neumí specializovat sama |
Jsou 2 instance lišící se pouze typovým parametrem stejným typem? |
Ano |
Ne |
Společný kód pro všechny instance |
Ano |
Ne (každá funkce je už během kompilace specializována) |
Typové parametry |
Jen jiná generika nebo přímo typy, nepovoluje se dávat šablonové typy do generik |
Cokoli |
Parciální a explicitní specializace |
Ne, Ne |
Ano, Ano |
Defaultní typový parameter |
Ne |
Ano |
Šablonové třídy jako typový parametr (template< template <class T1> class T2> |
Ne |
Ano |
Volání libovolných metod z typových parametrů (pokud jsou definovány) |
částečne pomocí constraintů |
Ano |
Jak je z tabulky vidět, tak šablony jsou výrazně silnější, neboť umožňují volat metody z typového parametry aniž by bylo potřeba dělat kvůli tomu rozhraní (neboť templates fungují z tohohle pohledu jako textový preprocesor, neboli se místo typového parametru T dosadí přímo ten typ a to se zkusí zkompilovat), používat netypové template parametry a další libůstky. Na druhou stranu generika jsou kompatibilnější, jak mezi jazyky tak i mezi C++/CLI assembly. Příklad použití template (tohle by s generiky neprošlo):
template <class T>
ref class TTrida
{
public:
void X() {
T::Staticka(); //volam statickou metodu T, pokud ji T má definovanou, vše je OK, jinak to skončí kompilační chybou, v případě generik to skončí chybou vždy
};
void Y() {
T ^x = gcnew T(); // v klidu vytvářím objekty, pokud by konstruktor nebyl definován, nepůjde to přeložit
x->y(); // volám metodu Tčka
}
};
ref class Xsi
{
public:
static void Staticka() {}
void y() {}
};
int main(array<System::String ^> ^args)
{
TTrida <Xsi> t;
t.X();
t.Y();
}
V případě generik si už tolik magie dovolit nemůžeme, pokud totiž pomocí constrantů neomezíme typové parametry na nějakou bázovou třídu a její potomky či interface, tak jediné, co můžeme o typovém parametru předpokládat, je to, že je typu System::Object. Má to i své výhody v lepší čitelnosti kódu – je hned jasné, že nemůžu jako typový parametr použít cokoli, ale jen to, co splňuje nějaká pravidla. V případě templates ty “pravidla” musím vyčíst z implementace. Constrainty nám umožňují omezit typové parametry jen na ty dědící z nějaké třídy nebo implementující nějaké rozhraní. Deklarují se klíčovým slovem where následovaným názvem typového parametru, dvojtečkou a onou bázovou třídou či interfacem.
interface class IWithX //definice rozhraní
{
public:
void X();
};
generic <class T> where T : IWithX // chci aby T implementovalo rozhraní IWithX
ref class GTrida
{
public:
void Funkce(T param) { param->X(); } // už můžu v klidu volat metodu X(), neboť vím, že T implementuje rozhraní IWithX
};
ref class Xsi : public IWithX
{
public:
virtual void X() override {}
};
int main(array<System::String ^> ^args)
{
GTrida <Xsi ^> t; //deklarace GTrida<XSi ^>
t.Funkce(gcnew Xsi());
}
Generické či šablonové samozřejmě nemusí být jen třídy, mohou to být i rozhraní, metody či delegáti. Deklarují se podobně jako generické třídy: template nebo generic konstrukt vždy patří na začátek, před vlastní deklaraci třídy, rozhraní, metody nebo delegáta.
Závěr
Shrnutím šablon a generik jsem zakončil tento článek přibližující základní myšlenky a syntaxi C++/CLI. Doufám, že se vám článek nejen líbil, ale také že vám přinesl nějaké nové vědomosti a rozhled v širém světě programátorském. Rozhodně to není vyčerpávající přehled všech featur a možností, od toho máme referenční dokumentaci na MSDN.
Přeji hodně štěstí při programování (nejen) na hranici .NET a nativního prostředí ;)