Úvod do jazyka a základní konstrukce

1. díl - Úvod do jazyka a základní konstrukce

Jakub Čermák       3. 9. 2009       C++/C, .NET       8112 zobrazení

V tomto článku je popsána nadstavba C++ pro práci s .NET prostředím zvaná C++/CLI umožňující vytvářed mixed assembly obsahující jak managed tak unmanaged kód. V prvním díle je popsána myšlenka jazyka a základní syntaktické konstrukty (základní typy, podmínky, cykly, pole, namespace a část tříd a objektů). U čtenáře je předpokládána znalost .NET frameworku a nativního programování nejlépe v C++ (alespoň syntaxi a základy).

Dnešní článek bude pro Vbnet trochu netypický – zatímco většina článků tu se zabývá čistě managed světem (nebo algoritmy) a slovíčka jako pointer či unsafe blok jsou tu vzácná, tento článek bude pointerů plný a něco jako ne-unsafe kód ani v C++/CLI nenajdete. Tento článek je určen pro programátory, kteří znají (aspoň trochu) jak managed (C#, VB.NET) tak i unmanaged (C++ aspoň syntakticky a nativní programování obecně) programování a rádi by tyto dva “světy” nějakým programmer-friendly způsobem spojili.

A co to vlastně to C++/CLI (C++/Common Language Infrastructure) je? Je to Microsoftí rozšíření (jinak čistě unmanaged) jazyka C++, které do něj přidává jazykové konstrukce a další věci nutné pro programování pro .Net framework, přičemž z něj nic neubírá (pokud nezvolíte pure MSIL, více dále). Takže už obsahuje knihovny třech jazyků – C, C++ a .NET, což samozřejmě na přehlednosti nepřidává, nicméně dává programátorovi do ruky velmi mocnou zbraň – kombinovat a přímo používat managed a unmanaged kód v jednom projektu. Což jiný .NET jazyk od MS neumí (mimo MS jazyky to tuším uměl/umí ObjectPascal v novějších Delphi).  Zkompilováním C++/CLI projektu vzniká tzv. mixed assembly, která obsahuje jak část v MSILu (včetně např. managed zapouzdření některých C++ tříd) tak část přímo ve strojovém (nativním) kódu. Managed třídy pak můžete používat (při dodržení určitých pravidel) i z jiných jazyků platformy .NET. Na závěr popisu ještě trošku historie - C++/CLI je obsažen až ve Visual Studiu 2005, 2003ka a nižší obsahovali tzv. Managed Extensions for C++, kterým chyběli některé featury a hlavně jejich syntaxe byla ještě chaotičtější než C++/CLI. MC++ je nyní deprecated a i když ho VS pořád podporuje, už se nedoporučuje v něm cokoli programovat.

Takže už víme, co to je, teď je třeba uvést, k čemu je to dobré. neboť, jak praví M. Valášek, neexistují špatné technologie, jsou jen technologie dobře použité a špatně použité.

  • Použití čistě unmanaged knihoven či funkcí je důvod, který mě “nutí” používat C++/CLI nejčastěji. .Netu chybí podpora pro pohodlnou práci s HW na trochu nižší úrovní, případně se sice k HW dodávají knihovny pro komunikaci, nicméně pouze Cčko. Samozřejmě tento problém se týká všech unmanaged-only knihovnen, ke kterým neexistuje vyhovující managed alternativa, nicméně se stále se zvětšujícím rozšířením .NETu se tyto knihovny moc nevyskytují (nebo člověk má příliš exotické požadavky či úkoly;) ).
  • Zrychlení výpočetního jádra se také občas hodí. I v .netu sice lze psát rychlé aplikace, ale často je to na úkor přehlednosti či “best-practices” managed platformy, která na to navíc není stavěná a člověk pak nese overhead GC a dalších managed featur, i když je zrovna při těch časově či paměťově náročných výpočtech vůbec nepotřebuje. Navíc C++ kompilátor generuje optimalizovanější kód než .netí JIT, což se projeví hlavně při nějakém složitějším kódu. Občas sice vidím ukázky toho, že .NET je stejně rychlý jako nativní kód psaný např. v C ale vždy na nějakém triviálním kódu, což je přinejmenším zavádějící.
  • A nebo naopak – mám nativní aplikaci v C++ a potřebuju k ní napsat nějaké pěkné GUI a nechce se mi zlobit se s nějakou více či méně dokonalou knihovnou na GUI v C++.
  • Připadně kdykoli jindy, když je potřeba nějak používat .NET z nativního kódu či naopak.

Samozřejmě v C++/CLI se dají psát i čistě managed aplikace, ale osobně to z důvodu vyšší komplikovanosti jazyka nedoporučuju, je lepší použít nějaký normální .NET jazyk.

Nicméně C++/CLi má kromě subjektivně složitější syntaxi i další nedostatky – zásadnim problémem .NET/native interoperability je nízká výkonnost. To lze pozorovat už i v P/invocích v “čistých” .NET jazycích – každý přechod mezi nativním a managed kódem s sebou nese jistou režii a v případě neopatrného používání může tento přechod nastávat často, což má pak vliv na výkonnost. Toto lze do jisté míry eliminovat striktím oddělováním managed a unmanaged části kódu a direktivami #pragma managed a #pragma unmanaged, nicméně je třeba přemýšlet. V neposlední řadě také C++/CLI způsobuje dost značnou obfuskaci výsledného kódu (což by někdo mohl vidět i jako výhodu …), jeden příklad uvedu později.

Kooperaci či interoperabilitu managed a unmanaged lze samozřejmě zajistit i jinými prostředky. Nejjednodušším příkladem jsou P/Invoky, kde ale nemůžu sdílet celé objekty, musím spravovat navíc ještě extern deklarace v managed jazyku a hlavně tím nezavolám managed kód z nativního programu. Lepší variantou jsou pak COM+ objekty, které jsou ale složitější, musí se registrovat a je s nimi obecně více práce. Na druhou stranu jsou obecnější.

Náš první C++/CLI projekt

Náš první projekt vytvoříme buď přímo přes klikátko New projekt v záložce Visual C++\CLR nebo ho uděláme z klasického C++ projektu nastavením Common Language Runtime support (co znamená Pure MSIL nebo safe MSIL je popsáno celkem pěkně na http://msdn.microsoft.com/cs-cz/library/85344whh%28en-us%29.aspx)image

Pro tento článek budeme zatím uvažovat konzolovou aplikaci vytvořenou přes klikátko (CLR Console application).

Základní syntaxe

Nově vytvořený projekt obsahuje jen main funkci, import System namespacu a výpis “Hello worldu” na konzoli. C++kaře na první pohled upoutají nejspíše ony divné operátory ^ vypadající jak z Pascalu, mě jako C#áře zarazily hlavně :: a to, že tam není klasická statická třída Program, ale jen funkce main.

Nejdřív by asi bylo dobré vysvětlit základní strukturu projektu. I přes .NETí příchuť  si projekt zachovává vzezření klasického Visual C++ projektu – máme klasické dělení na hlavičkové a .cpp soubory, máme stdafx.h, kam se píší standardní a knihovní hlavičky, které si kompilátor umí předkompilovat a nemusel je kompilovat při každém #include “stdafx.h”, čímž se kompilace výrazně urychlí. Preprocesor se chová stejně jako v C++, jen navíc přidává #using pro referencování jiných assembly. Ty je možné referencovat dvojím způsobem, buď v nastavení projektu v Common Properties nebo pomocí #usingů, které se používají #using <NázevAssembly> a píší se přímo do cpp kódu.

Základní typy

Základní typy jako int, double či char jsou přímo mapovány na jejich .NET ekvivalenty (koneckonců int či double vypadá v paměti stejně jak v .netu tak v C++). To se týká jak generických typů (tj. můžu psát System::Collections::Generic::List<int> stejně jako std::vector<int>), tak i statických metod a vlastností, které tyto typy v .NETu mají (tj můžu psát int::Parse()). Přehled mapování je v následující tabulce:

Visual C++ typ

.NET Framework typ

bool

System.Boolean

signed char

System.SByte

unsigned char

System.Byte

wchar_t

System.Char

double, long double

System.Double

float

System.Single

int, signed int, long, signed long

System.Int32

unsigned int, unsigned long

System.UInt32

__int64, signed __int64

System.Int64

unsigned __int64

System.UInt64

short, signed short

System.Int16

unsigned short

System.UInt16

void

System.Void

S řetězci je to již trošku složitější – v paměti sice vypadají podobně, ale rozhodně ne stejně. Navíc System::String je referenční typ a tak sedí v managed haldě. Tudíž nemohu jen tak přiřadit přímo System::String do char * či opačně. Ale mám několik možností, jak toho dosáhnout nepřímo:

  • Můžu použít třídu System::Marshal, která se používá pro obecnou interoperabilitu mezi managed a unmanaged
  • Nebo pro převod managed->unmanaged použiju funkci PtrToStringChars definovanou v <vcclr.h>
     String ^str =gcnew String("ahoj"); //vytvorim instanci System::Stringu
    pin_ptr<const wchar_t> str2 = PtrToStringChars(str); // prevod
    std::wcout << (wchar_t *)str2; // vypsani
    Syntaxi pořádně popíšu později, důležité je vědět a pamatovat, že PtrToStringChars nekopíruje paměť a proto str2 ukazuje kamsi doprostřed objektu str (který leží na managed haldě). Proto je potřeba objekt “připíchnout” pomocí pin_ptr, aby nám ho GC nikam nešoupl. Detaily o pin_ptr a interior_ptr popíšu později.
  • Pokud potřebujeme udělat z unmanaged stringu System::String, tak máme situaci jednoduchou, neboť máme pro tento případ jedno přetížení konstruktoru String.

Podmínky a cykly

If podmínky, Switch větvení, For, While a do .. while cykly jsou uplně stejné jako C++, novinkou je for each cyklus, což je ekvivalent C# foreach či Vb.Net ForEach cyklů. Umí procházet IEnumerable kolekce, unmanaged pole a dokonce i STL kontejnery. Má tvar for each(typ iterační_proměnná in kolekce). Oproti C# či VbNet foreachi ale umí jednu užitečnou věc – nemusíme iterovat hodnotou ale můžeme i referencí, takže můžeme přímo ve foreachi měnit prvky kolekce nejen jejich obsah. V příkladu to používám pro vytvoření jagged pole (pole polí). Reference se zapisuje pomocí operátoru % a ještě o ní bude reč dále.

 int main(array<System::String ^> ^args)
{
     std::vector<
int> vect; //stvoříme std::vector
    vect.push_back(4); vect.push_back(6); // naplníme ho
    for each(int i in vect) // a vypíšeme
        Console::WriteLine(i); // ....

    // vyvoření jagged array pomocí foreache
    array<array<int> ^> ^jagged = gcnew array<array<int> ^>(5); //vytvoření v vnějšího pole
    for each (array<int> ^% arr in jagged) // enumerace vytvoření vnitřního pole
        arr = gcnew array<int>(6);
}

Pokud by vás zajímalo, jak je to interně řešené, tak vězte, že nikterak pěkně. Stačí si vzít Reflector a podívat se. Následující kód je v skoro-C# i když to tak na první pohled nevypadá. Konstrukci try … fault  běžně neuvidíte, slouží pro obsluhu a práci se Structured Exception Handling, což je mechanismus Windows jak řešit chyby jako je špatný přístup do paměti. Je také pěkně vidět, jak mají Microsofti definice STL tříd i v .NETu.

 internal static unsafe int main(string[] args)
{
int[][] $S4 = null;
int[][] jagged = null;
vector<
int,std::allocator<int> > vect;
std.vector<
int,std::allocator<int> >.{ctor}(&vect);
try
{
_Vector_const_iterator<
int,std::allocator<int> > $S2;
_Vector_iterator<
int,std::allocator<int> > local2;
int modopt(IsConst) num2 = 4;
std.vector<
int,std::allocator<int> >.push_back(&vect, &num2);
int modopt(IsConst) num = 6;
std.vector<
int,std::allocator<int> >.push_back(&vect, &num);
vector<
int,std::allocator<int> >* modopt(IsImplicitlyDereferenced) $S1 = &vect;
_Vector_iterator<
int,std::allocator<int> >* localPtr2 = std.vector<int,std::allocator<int> >.end($S1, &local2);
try
{
std._Vector_const_iterator<
int,std::allocator<int> >.{ctor}(&$S2, (_Vector_const_iterator<int,std::allocator<int> > modopt(IsConst)* modopt(IsImplicitlyDereferenced)) localPtr2);
}
fault
{
___CxxCallUnwindDtor(std._Vector_iterator<
int,std::allocator<int> >.{dtor}, (void*) &local2);
}
try
{
_Vector_const_iterator<
int,std::allocator<int> > $S3;
_Vector_iterator<
int,std::allocator<int> > local;
std._Vector_iterator<
int,std::allocator<int> >.{dtor}(&local2);
_Vector_iterator<
int,std::allocator<int> >* localPtr = std.vector<int,std::allocator<int> >.begin($S1, &local);
try
{
std._Vector_const_iterator<
int,std::allocator<int> >.{ctor}(&$S3, (_Vector_const_iterator<int,std::allocator<int> > modopt(IsConst)* modopt(IsImplicitlyDereferenced)) localPtr);
}
fault
{
___CxxCallUnwindDtor(std._Vector_iterator<
int,std::allocator<int> >.{dtor}, (void*) &local);
}
try
{
std._Vector_iterator<
int,std::allocator<int> >.{dtor}(&local);
goto Label_0089;
Label_0081:
std._Vector_const_iterator<
int,std::allocator<int> >.++(&$S3);
Label_0089:
if (std._Vector_const_iterator<int,std::allocator<int> >.!=((_Vector_const_iterator<int,std::allocator<int> > modopt(IsConst)* modopt(IsConst) modopt(IsConst)) &$S3, (_Vector_const_iterator<int,std::allocator<int> > modopt(IsConst)* modopt(IsImplicitlyDereferenced)) &$S2))
{
int i = *(std._Vector_const_iterator<int,std::allocator<int> >.*((_Vector_const_iterator<int,std::allocator<int> > modopt(IsConst)* modopt(IsConst) modopt(IsConst)) &$S3));
Console.WriteLine(i);
goto Label_0081;
}
}
fault
{
___CxxCallUnwindDtor(std._Vector_const_iterator<
int,std::allocator<int> >.{dtor}, (void*) &$S3);
}
std._Vector_const_iterator<
int,std::allocator<int> >.{dtor}(&$S3);
}
fault
{
___CxxCallUnwindDtor(std._Vector_const_iterator<
int,std::allocator<int> >.{dtor}, (void*) &$S2);
}
std._Vector_const_iterator<
int,std::allocator<int> >.{dtor}(&$S2);
jagged =
new int[5][];
$S4 = jagged;
int $I = 0;
goto Label_00E8;
Label_00E4:
$I++;
Label_00E8:
if ($I < $S4.Length)
{
ref int[] arr = &($S4[$I]);
arr =
new int[6];
goto Label_00E4;
}
}
fault
{
___CxxCallUnwindDtor(std.vector<
int,std::allocator<int> >.{dtor}, (void*) &vect);
}
std.vector<
int,std::allocator<int> >.{dtor}(&vect);
return 0;
}

Namespaces

.Net namespacy se používají úplně stejně jako klasickém C++, tj. import namespace, abychom je nemuseli vždy vypisovat (using konstrukce v C#) probíhá pomocí using namespace <Namespace>, umí to samozřejmě i vnořené namespacy, using namespace System::Drawing, operátor :: (scope resolution operátor, pro přístup ke statickým prvkům tříd k a explicitnímu vypisování namespacu k nějakému typu/funkci, PHPkářům jistě známý jako T_PAAMAYIM_NEKUDOTAYIM) je jistě všem C++ programátorům důvěrně známý. Pokud chceme, aby náš kód nebyl v defaultním namespacu, musíme uzavřít kód do konstrukce namespace Neco { … } (stejně jako v C++ a C#). Otravná nevýhoda tohoto konstruktu v C++ je, že neumí vnořený namespace napsat najednou, pokud chci tedy psát kód v Cermi::Aplikace namespace musím udělat:

 namespace Cermi
{
    
namespace Aplikace
     {
        
// muj kod
    }
}

    Pole

    Managed a unmanaged pole jsou (stejně jako  retězce) 2 rozdílné věci a proto máme v C++/CLI obojí. Unmanaged pole se deklarují a používají stejně jako v C++

         int upole[20]; //pole na stacku
        int *upole2 = new int[20]; // pole na haldě
        Console::WriteLine(upole[5]); // vypíše náhodnou hodnotu, pole není inicializované
        int *ptr = upole + 4; //ukazatel na 5. prvek
        ptr++; //ptr je ted ukazatel na 6. prvek

    Managed pole se vytváří pomocí klíčového slova array, které vypadá jako template třída array s 2 template parametry – první je typ pole, druhý je číslo značící počet dimenzí. Typ pole může být handle na refereční typ (type ^) či hodnotový managed typ nebo pointer (type *). Nesmí to být žádný nejednoduchý nativní typ. Obecná syntaxe je následující:

    [qualifiers] array<[qualifiers]type1[, dimension]>^var = gcnew array<type2[, dimension]>(val[,val...])

    , kde qualifiers  jsou kvalifikátory jako const či static, type1 je formální typ pole, dimension je dimenze pole, type2 skutečný typ pole a val1valn jsou rozměry jednotlivých dimenzí pole. Type1 a type2 jsou většinou stejné, vždy ale platí, že musí existovat konverze z Type2 do Type1. Pole podporují tzv. kovarianci, což znamená že pokud mám pole typu A a pole typu B a existuje konverze z A do B, tak můžu přiřadit pole A do pole B,

         array<String ^> ^mpole = gcnew array<String ^>(4); // pole 4 řetězců
    mpole[0] = "Ahoj"; // přístup k prvku pole
        Console::WriteLine(mpole->Length); //vypiseme delku pole

        array<int *,3> ^mpole2 = gcnew array<int *, 3>(2,3,4); //vytvoření 3dimenzinálního pole
        mpole2[0,0,0]=NULL; // přístup k prvku pole

    Můžu také vytvářet zubatá neboli jagged arrays, což je vlastně pole polí a tak se taky i zapisuje:

     array<array<int> ^> ^jagged = gcnew array<array<int> ^>(5); //vytvoření v vnějšího pole
    for each (array<int> ^% arr in jagged) //vytvoření vnitřního pole
        arr = gcnew array<int>(6);

    Třídy a struktury

      Jak všichni víme, tak C++ podporuje jeden typ třídy (je jedno jestli je zapsaný jako class nebo struct), který mohu vytvořit buď na stacku nebo na haldě (co je zásobník a halda popsal Tomáš ve svém článku o základech platformy .NET), zatímco .NET rozlišuje referenční typy (class) a hodnotové typy (struct). V první řadě je třeba si uvědomit, že máme jeden stack společný pro managed a unmanaged objekty, ale 2 různé haldy – jednu pro managed a druhou pro unmanaged objekty. Také nesmíme mixovat nativní a managed typy – tj. nemůžu jako member proměnnou v managed typu mít nějakou nativní či naopak (i když tam to jde obejít pomocí gcroot, což popíšu v dalším díle).

      Handles

      Protože nativní ukazatel na managed objekt nespolupracuje s GC (a ani ho přímo nemůžu vytvořit), tak se zavedly tzv. handles (českým ekvivalentem jsi nejsem jist), což jsou jiné handles než ty, které se používají jako “identifikátory” objektů Windows (jakýkoli objekt OS používaný z userspacu má své handle, pomocí něhož se s ním manipuluje). Vzhledem k tomu, že Win32 handly zde nebudu používat, tak pojem handle  bude znamenat vždy handle na managed objekt. Když máme ujasněnou terminologii, tak vysvětlím, co handle znamená. Je to něco, co ukazuje na celý objekt na managed haldě, tady v podstatě klasická reference z C# či VB.NETu:

      Kód v jazyce C#
       StringBuilder sb = new StringBuilder();

      Slovo “celý” jsem zvýraznil proto, že existují ještě tzv. tracking references, které mohou ukazovat i na nějakou členskou proměnnou managed objektu. Důležité je to, že ukazuje na objekt a ne na kus paměti. Protože GC může, pokud uzná za vhodné, objekty přesouvat po paměti, tak ukazatel na paměť, kde se nachází objekt, je nám v podstatě k ničemu (pokud objekt “nepřipíchneme”, viz další díl), neboť nám nikdo nezaručí, že objekt se na dané adrese bude nacházet i v příští sekundě. Handles ale s GC “spolupracují” a tak se o toto nemusí starat a ukazují na objekt, ať už se nachází na jakékoli adrese.

      Handles se deklarují pomocí operátoru ^ (stříška), který se píše před název proměnné. Pokud chceme přistupovat k prvkům objektu, na který máme handle, tak použijeme operátor –> stejně jako když přistupujeme k nativnímu objektu, na který mám ukazatel (nativní ukazatel, v tomto článku budu pojmem ukazatel vždy rozumět “hloupý” nativní ukazatel na nějakou adresu v paměti). Handle můžeme i dereferencovat pomocí operátoru * (hvězdička).

      Unmanaged třídy

      Unmanaged třídy se chovají a deklarují uplně stejně jako v C++, zde se nic nemění.

      Hodnotový managed typ

      Hodnotový managed typ se deklaruje pomocí klíčového slůvka value před slovem class nebo struct. Stejně jako v C++ platí, že mezi struct a class je pouze jediný rozdíl a to v tom, že členové bez určení viditelnosti jsou u class private, kdežto u struct public. Vytváří se na stacku, nicméně můžeme pomocí gcref vytvořit i jeho instanci na managed haldě (to ani v C# ani VbNetu nejde, pokud se nepletu).

       value class Struktura
      {
      public:
          
      int A,B;
      };
      int main(array<System::String ^> ^args)
      {
          
      int x = 0x78563412;
           Struktura s;
           s.B = 5;
      return 0;
      }

      Z výpisu z paměti (červeně je x, zeleně s.A, modře s.B) je také vidět, že member proměnné v managed typech se inicializují na defaultní hodnotu:

      image

      Referenční managed typ

      Rerefenční managed typy se deklarují pomocí slova ref před slovem class nebo struct. Instance se vytváří pomocí operátoru gcnew, což je new pro managed typy, a vrací handle na nově vytvořenou instanci. Ve starém Managed C++ se gcnew nepoužívalo, bylo všude pouze new, takže nebylo přímo z kódu jasné, jestli se dělá managed nebo unmanaged instance. Referenční typy se vždy vytváří na managed haldě. Je možné při deklaraci instancí použít i syntaxi pro vytváření struktur na stacku – instance se ale stejně interně vytvoří na managed haldě, ale používá se trochu jinak.

       ref class Trida
      {
      public:
          
      int A,B; //deklarace promennych
          void VypisA() { std::cout << A << std::endl; } // deklarace funkce
          void VypisB(); //druha moznost deklarace fce
      };

      void zmenA(Trida ^x)
      {
           x->A = 7;
      }

      void Trida::VypisB()
      {
           std::cout << B << std::endl;
      }

      int main(array<System::String ^> ^args)
      {
           Trida ^t =
      gcnew Trida();
           t->VypisA();
      //vypise 0
          t->A = 5;
           t->VypisA();
      //vypise 5
          zmenA(t);
           t->VypisA();
      //vypise 7
      }

      Konstruktory a destruktory

      Konstruktor se definuje jako C++ – jako metoda bez návratového typu se stejným názvem, jako má třída. Statický konstruktor máme také a definuje se jako statický konstruktor (nevím jak lépe to popsat, snad příkladem)

       ref class Trida
      {
      public:
          
      static int X;
          
      static Trida()
           {
               X=7;
           }
      };

      Destruktor, který se definuje také stejně jako v C++ (tj. ~NázevTřídy), ale u managed objektů není destruktorem v pravém slova smyslu, neboť managed objekty žádný nemají. Je to totiž do samé, jako kdybyste implementovali metodu Dispose z IDisposable rozhraní. Podle MSDN dokumentace se nedoporučuje implementovat IDisposable přímo, ale unmanaged resources uvolňovat právě pomocí destruktoru. Destruktory se volají deterministicky (narozdil od finalizeru), pokud nastane nějaká z následujících situací:

      • Objekt vytvořený pomocí “stack semantics” (tj. tak jak se vytvářejí hodnotové typy na stacku, bez gcnew) se zničí, když aplikace vyskočí z bloku, kde byl definován, ať už normálně nebo během stack unwinding při “probublávání” výjimky
      • Při vyhození výjimky v konstruktoru
      • Když se destruuje objekt, jehož je member proměnnou (nadeklarovanou přímo, ne jako handle nebo pointer)
      • Voláním operátoru delete na handle daného objektu
      • Voláním Dispose
      • Explicitním voláním desktoru

      C++/CLI samozřejmě podporuje i finalizery – deklarují se jako metoda bez návratového typu se jménem
      !NázevTřídy. Jsou to stejné finalizery jako je známe z jiných .NET jazyků, tj. provádí se při ničení objektu Garbage Collectorem (pokud to není vypnuté, proto se na finalizery nevyplatí spoléhat).

      Pokračování příště …

      V dalším díle (a pravděpodobně posledním) dokončíme povídání o třídách a popíšeme eventy, pointery, reference a další důležité prvky C++/CLI.

       

      hodnocení článku

      2 bodů / 2 hlasů       Hodnotit mohou jen registrované uživatelé.

       

      Všechny díly tohoto seriálu

      2. Složitější konstrukty, low-level přístup a generické programování 14. 9. 2009
      1. Úvod do jazyka a základní konstrukce 3. 9. 2009

       

      Mohlo by vás také zajímat

      Genericita, rozhraní a dědičnost

      Jazyk C# je multiparadigmatický, což v praxi znamená, že v něm můžeme dělat hodně věcí. Jak ale do sebe jednotlivá paradigma zapadají? Co se hezky doplňuje a co není vzájemně kompatibilní? V tomto článku chci popsat, jak se chová IEquatable vzhledem k dědičnosti typu T.

      Jednoduchý scheduler v .NETu

      Asi to znáte – máte nějaký složitější systém na zpracování velkého objemu dat a čas od času potřebujete vykovat nějakou automatizovanou údržbu – typicky smazat všechny položky starší než několika dní. Možností, jak toho dosáhnout, je hodně. Snažil jsem se vymyslet něco jednoduchého a efektivního.

      Jeden antipattern, který dokáže asynchronní programování pořádně znepříjemnit

       

       

      Nový příspěvek

       

      Diskuse: Spojujeme native a .NET světy: C++/CLI (1. díl)

      Dobrý článek, nesouhlasím však s tvrzením, že C++ kompilátor vytváří optimalizovanější kód než .NET JIT kompilátor. Už jen z principu, že .NET JIT kompilátor optimalizuje kód přímo pro platformu na které je kód prováděn, kdežto C++ kompilátor to musí dělat obecně.

      nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

      Souhlasit nemusíte, realita je ovšem taková. JIT kompiluje jen kód, který je výslovně potřeba, a vzhledem k tomu, že při spouštění aplikace na to nemá tolik času, není zkrátka možné, aby prováděl všechny optimalizace. Když si dáte zkompilovat nějaký větší projekt v C++, kompilace trvá klidně hodinu. .NET aplikace se ve visual Studiu kompiluje jen do MSILu, což je de facto spíš jiný způsob zápisu zdrojového kódu než skutečná kompilace, samotný JIT má na to pár sekund. Nestíhá dělat komplexní optimalizace, jako třeba inlinování metod, operátorů, vlastností, dělá jen jednodušší věci. To, že optimalizuje pro daný procesor, rychlost aplikace zase o něco zvyšuje, ale nedostatek času pro provádění optimalizací má daleko vyšší význam.

      Nechci tím tvrdit, že .NET je obecně pomalý, ze všech managed prostředí, která existují, je .NET rozhodně nejrychlejší a ve většině případů se jeho použití vyplatí - o jednotky procent nižší rychlost většinou nad rychlostí vývoje a bohaté knihovně funkcí nevyhraje. Samozřejmě na jisté typy úloh se .NET vyloženě nehodí.

      nahlásit spamnahlásit spam 0 odpovědětodpovědět

      Btw ta syntaxe je místy dost divoká. Jako když se nad tím pořádně zamyslím, proč je tam to a to, tak je to pak jasné, ale na první pohled to v tom rozhodně nevidím. Asi je to o zvyku.

      Ale článek velmi dobrý.

      nahlásit spamnahlásit spam 2 / 2 odpovědětodpovědět

      To je holt daň za to, že je tam hodně věcí 2x. Ale aspoň je jasně vidět, co je C++ a co je .NET, což třeba u Managed Extensions (předchůdce C++/CLI) často na první pohled vidět nebylo.

      nahlásit spamnahlásit spam 0 odpovědětodpovědět

      Přesně jak říká Tomáš - menší čas na optimalizace je důležitější než zaměření na konkrétní platformu JITem, hodně se to projeví u složitějšího kódu. Samozřejmě, určitě se povede najít takový příklad, kdy výhody JIT kompilace převáží. Navíc i v C++ můžete kompilovat pro několik architektur - zvlášť pro "kompatibilní" i386, zvlášť pro stroje s SSE3 instrukcemi, pro IA64, atd atd.

      Navíc managed aplikace obecně jsou paměťově náročnější, což má také vliv na rychlost, neboť je více cache missů. Samozřejmě nemá cenu řešit jednotky procent, neboť to je v podstatě statistická a systematická chyba (na každém CPU to bude jiné...). Hodně záleží na typu úlohy a dalších faktorech. Když člověk potřebuje super rychlost, tak použije realtime systém nebo třeba si spájí nějaký jednoúčelový obvod :)

      nahlásit spamnahlásit spam 0 odpovědětodpovědět

      JIT kompilátor je rychlý dostatečně a kromě toho lze provést optimalizaci nástrojem ngen.exe, potom už žádný překlad v průběhu činnosti programu neprobíhá.

      nahlásit spamnahlásit spam 0 odpovědětodpovědět

      Ano, ovšem to nic nemění na tom, že JIT i NGen tolik optimalizací jako C++ kompilátor ani neumí, protože je prostě v řádu jednotek sekund nemůžou stihnout provést.

      Až od .NET Frameworku 4 bude například pořádně podporována tail rekurze (kvůli funkcionálnímu F#), na 64bitových systémech kompilátory až dosud třeba vůbec neinlineovaly a takových restíků je tam mnoho. Takových restíků je tam daleko víc. Nový runtime by měl být tedy znatelně rychlejší, protože velkou část z nich řeší.

      nahlásit spamnahlásit spam 0 odpovědětodpovědět

      Nový runtime by měl být tedy znatelně rychlejší

      To říkají v Microsoftu vždycky :), ale je třeba podotknout, že vždy o něco rychlejší je, třeba mezi 1 a 2kou ten rozdíl byl znatelný (pozorováno okem na celkem starých stanicích, kde je každá optimalizace viditelná).

      Já osobně zastávám názor, že (přehnaně řečeno) u frameworku a knihoven není žádná optimalizace dostatečná, neboť tvůrce knihovny nikdy nemůže vědět, k jakému účelu ji použije. Navíc zrychlením .NET frameworku se zrychlí obrovské množství aplikací, které na něm závisí, což už se vyplatí.

      nahlásit spamnahlásit spam 0 odpovědětodpovědět

      U ngenu na rychlosti vůbec nezáleží, protože kompilace a optimalizace MSIL kódu do nativního kódu se provede pouze jednou, typicky po instalaci aplikace. Při běhu programu už potom není třeba nic dokompilovávat.

      nahlásit spamnahlásit spam 0 odpovědětodpovědět

      Ano, to je pravda a pokud se to udělá při instalaci, kód je zoptimalizovaný pro daný procesor. Ale to nic nemění na tom, že ani ngen neumí tolik optimalizací, co dobrý C++ kompilátor. Není to sice tak hrozné jako JIT, ale rezervy tam rozhodně jsou.

      nahlásit spamnahlásit spam 0 odpovědětodpovědět

      On ale ani ten C++ kompilátor z Visual Studia nestojí za nic. Skutečně kvalitní kompilátory kde jsou optimalizace na prvním místě jsou kolikrát dražší než celé Visual Studio. Příkladem budiž kompilátor od Intelu - Intel Compiler Suite Professional Edition for Windows. V tomto případě se jedná o skutečnou optimalizaci.

      nahlásit spamnahlásit spam 0 odpovědětodpovědět
                             
      Nadpis:
      Antispam: Komu se občas házejí perly?
      Příspěvek bude publikován pod identitou   anonym.

      Nyní zakládáte pod článkem nové diskusní vlákno.
      Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

      Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

       

      • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
        Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
      • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

      přihlásit pomocí externího účtu

      přihlásit pomocí jména a hesla

      Uživatel:  
      Heslo:  

      zapomenuté heslo

       

      založit nový uživatelský účet

      zaregistrujte se

       
      zavřít

      Nahlásit spam

      Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

      Nahlásit Zrušit

      Chyba

      zavřít

      feedback