Sdílet prostřednictvím


Zápis přetypování a představení safe_cast<>

Zápis přetypování se změnil ze spravovaných rozšíření jazyka C++ na Visual C++ 2010.

Úprava existující struktury je rozdílná a obtížnější možnost než vytvoření počáteční struktury. Existuje zde menší volnost a toto řešení inklinuje ke kompromisu mezi ideální restrukturalizací a tím, co je prakticky dáno existujícími strukturálními závislostmi.

Rozšíření jazyka je další příklad. V raných devadesátých letech, s tím jak se objektově orientované programování stávalo důležitým paradigmatem, stávala se potřeba pro downcast, zajišťující bezpečnost typů, v C++ tíživou. Downcastování je uživatelem vyvolaný převod ukazatele základní třídy nebo odkazu na ukazatel nebo odkaz odvozené třídy. Downcastování vyžaduje explicitní vyvolání. Důvodem je, že skutečný typ ukazatele základní třídy je aspektem modulu runtime, kompilátor jej proto nemůže zkontrolovat. Nebo jinak řečeno, zařízení downcast, stejně jako volání virtuální funkce, vyžaduje určitou formu dynamického rozlišení. To vyvolává dvě otázky:

  • Proč by měl být downcast nutný v objektově orientovaném paradigmatu? Není mechanismus virtuální funkce dostatečný? To znamená, proč nemůžeme tvrdit, že jakákoli potřeba downcastu (nebo převodu jakéhokoli druhu), je chybou návrhu?

  • Proč by měla být podpora downcastu problémem v jazyce C++? Toto přece není problém v objektově orientovaných jazycích jako je Smalltalk (nebo následně Java a C#)? Co způsobuje podporu downcastu v jazyce C++ obtížnou?

Virtuální funkce reprezentuje algoritmus závislý na typu, společný rodině typů. (Neuvažujeme rozhraní, která nejsou podporována v ISO-C++, ale jsou k dispozici v CLR programování, a která představují zajímavou alternativu návrhu). Návrh této rodiny je obvykle reprezentován hierarchií tříd, ve které je abstraktní základní třída, deklarující společné rozhraní (virtuální funkce) a sadu konkrétních odvozených tříd, které představují skutečné typy rodiny v doméně aplikace.

Hierarchie Light v Computer Generated Imagery (CGI) bude mít například společné atributy jako color, intensity, position, on, off atd. Člověk může řídit několik světel pomocí společného rozhraní bez obav, zda je konkrétní světlo bodové, směrové, nesměrové (uvažujeme o slunci) nebo případně reflektor. V tomto případě je zbytečné downcastovat na typ konkrétního světla pro vykonání jeho virtuálního rozhraní. V provozním prostředí je však rychlost základem. Člověk může downcastovat a explicitně vyvolat každou metodu a pokud takto učiní, může být provedeno vložené volání namísto použití virtuálního mechanismu.

Jedním z důvodů pro downcast v jazyce C++ je tedy potlačení virtuálního mechanismu pro významný zisk výkonu při běhu. (Všimněte si, že automatizace této ruční optimalizace je aktivní oblastí výzkumu. Vyřešení je však obtížnější než nahrazení explicitního využívání klíčového slova register nebo inline.)

Druhý důvod pro downcast vyplývá z duální povahy polymorfismu. Jedním ze způsobů, jak můžete uvažovat o polymorfismu, je rozdělení na pasivní a dynamické páry forem.

Virtuální vyvolání (a zařízení downcast) představuje dynamické použití polymorfismu: člověk provádí akci v závislosti na skutečném typu ukazatele základní třídy v dané konkrétní instanci při provádění programu.

Přiřazení objektu odvozené třídy k jeho ukazateli základní třídy je však pasivní forma polymorfismu; je to použití polymorfismu jako transportního mechanismu. Toto je hlavní použití Object, například v předobecném programování CLR. Při pasivním použití ukazatel základní třídy, zvolený pro transport a skladování, obvykle nabízí rozhraní, které je příliš abstraktní. Object například poskytuje přibližně pět metod prostřednictvím svého rozhraní; jakékoli další specifické chování vyžaduje explicitní downcast. Například pokud chceme upravit úhel našich světel nebo jejich rychlost opadnutí, musíme provést explicitní downcast. Virtuální rozhraní v rámci rodiny podtypů prakticky nemůže být nadmnožinou všech možných metod jeho mnoha potomků, a tak bude zařízení downcast vždy potřebné v rámci objektově orientovaného jazyka.

Pokud je potřeba bezpečné zařízení downcast v objektově orientovaném jazyku, proč trvalo C++ tak dlouho jej přidat? Problém je, jak zpřístupnit informace o typu ukazatele v době spuštění. U virtuální funkce jsou informace o spuštění nastaveny kompilátorem ve dvou částech:

  • Objekt třídy obsahuje další člen ukazatele virtuální tabulky (buď na začátku nebo konci objektu třídy; tento v sobě samém obsahuje zajímavou historii), který adresuje příslušnou virtuální tabulku. Například objekt bodového světla adresuje virtuální tabulku bodového světla, směrové světlo virtuální tabulku směrového světla atd

  • Každá virtuální funkce má přidružené pevné místo v tabulce a skutečná instance pro vyvolání je představována adresou, uloženou v tabulce. Například virtuální destruktor Light může být přidružen k pozici 0, Color k pozici 1 a tak dále. To je účinná strategie, pokud je pevná, protože je nastavena v čase kompilace a představuje minimální režii.

Problémem potom je, jak udělat informace o typu dostupné ukazateli bez změny velikosti ukazatelů C++, buď přidáním druhé adresy nebo přímým přidáním nějakého druhu kódování typů. To by nebylo přijatelné pro programátory (a programy), kteří se rozhodnou nepoužívat objektově orientované paradigma, což byla stále dominantní komunita uživatelů. Další možností bylo zavést zvláštní ukazatel pro polymorfní typy tříd, ale to by bylo matoucí a ztěžovalo by to prolínání obojího, zejména s problémy aritmetiky ukazatelů. Nebylo by rovněž přijatelné udržovat tabulku za běhu, která přidružuje každý ukazatel s jeho aktuálně přiřazeným typem a dynamicky ji aktualizovat.

Problém potom je několik komunit uživatelů, které mají odlišné, ale legitimní programovací aspirace. Řešením musel být kompromis mezi dvěma komunitami, umožňující každé nejen jejich aspiraci, ale schopnost spolupráce. To znamená, že řešení nabízená oběma stranami jsou neproveditelná a implementované řešení nakonec nebude dokonalé. Skutečné řešení se pohybuje kolem definice polymorfní třídy: polymorfní třída je taková, která obsahuje virtuální funkci. Polymorfní třída podporuje dynamický typově bezpečný downcast. Toto řeší problém udržení ukazatele jako adresy, protože všechny polymorfní třídy obsahují další člen ukazatele k jejich přidružené virtuální tabulce. Přidružené informace typu proto mohou být uloženy v rozšířené struktuře virtuální tabulky. Cena typově bezpečného downcastu je (téměř) lokalizovaná k uživatelům zařízení.

Dalším problémem s typově bezpečným downcastem byla jeho syntaxe. Protože je to přetypování, původní návrh pro komisi ISO-C++ použil prostou syntaxi přetypování jako v následujícím příkladu:

spot = ( SpotLight* ) plight;

ale toto bylo odmítnuto výborem, protože to neumožňovalo uživateli řídit cenu přetypování. Pokud měl dynamický typově bezpečný downcast stejnou syntaxi jako dřívější nebezpečný, ale statický zápis přetypování, pak se jednalo o nahrazení a uživatel neměl schopnost potlačit režii při běhu, kdy to nebylo potřebné a pravděpodobně příliš nákladné.

Obecně je v jazyce C++ vždy mechanismus, jak potlačit funkce podporované kompilátorem. Například můžeme vypnout virtuální mechanismus buď pomocí operátoru rozsahu třídy (Box::rotate(angle)) nebo vyvoláním virtuální metody prostřednictvím objektu třídy (spíše než ukazatele nebo odkazu této třídy). Toto potlačení jmenované jako druhé není vyžadováno jazykem, ale je kvalitou implementačního problému, podobně jako potlačení konstrukce dočasného v deklaraci formou:

// compilers are free to optimize away the temporary
X x = X::X( 10 );

Návrh byl tedy vzat zpět k dalšímu zvážení a bylo uváženo několik alternativních notací a ta, která byla navrácena komisi byla ve formě (?type), která indikovala jeho neurčenou – tj. dynamickou povahu. To dalo uživateli možnost přepnout mezi dvěma formami – statickou nebo dynamickou – ale nikdo s tím nebyl příliš spokojen. Vrátilo se to tedy zpět na rýsovací prkno. Třetí a úspěšný zápis je nynější standard dynamic_cast<type>, který byl generalizován na sadu čtyř notací přetypování nového stylu.

V ISO-C++, dynamic_cast vrací 0 při použití na nevhodný typ ukazatele a vyvolá výjimku std::bad_cast při použití na odkazový typ. Ve spravovaných rozšířeních jazyka C++ použití dynamic_cast na spravovaný odkazový typ (z důvodu jeho znázornění ukazatelem) vždy vrací 0. __try_cast<type> byl představen jako analogie s variantou vyvolávající výjimky dynamic_cast, s tím rozdílem, že vyvolává System::InvalidCastException pokud přetypování selže.

public __gc class ItemVerb;
public __gc class ItemVerbCollection {
public:
   ItemVerb *EnsureVerbArray() [] {
      return __try_cast<ItemVerb *[]>
         (verbList->ToArray(__typeof(ItemVerb *)));
   }
};

V nové syntaxi byl __try_cast přepracován jako safe_cast. Zde je stejný fragment kódu v nové syntaxi:

public ref class ItemVerb;
public ref class ItemVerbCollection {
public:
   array<ItemVerb^>^ EnsureVerbArray() {
      return safe_cast<array<ItemVerb^>^>
         ( verbList->ToArray( ItemVerb::typeid ));
   }
};

Ve spravovaném světě je důležité povolit ověřitelný kód omezením schopnosti programátorů přetypovávat mezi typy způsoby, které ponechají kód neověřitelný. To je kritické hledisko paradigmatu dynamického programování, reprezentované novou syntaxí. Z tohoto důvodu jsou instance přetypování starého stylu interně přetypovány jako přetypování za běhu tak, aby například:

// internally recast into the 
// equivalent safe_cast expression above
( array<ItemVerb^>^ ) verbList->ToArray( ItemVerb::typeid ); 

Na druhé straně, protože polymorfismus poskytuje jak aktivní tak pasivní režim, je někdy nutné provést downcast jen pro získání přístupu k nevirtuálnímu rozhraní API podtypu. Tato situace může nastat například u člena (členů) třídy, která chce adresovat jakýkoli typ v rámci hierarchie (pasivní polymorfismus jako mechanismus přenosu), ale pro kterou je skutečná instance v rámci konkrétního kontextu programu známá. V tomto případě může být režie kontroly přetypování za běhu nepřijatelná. Má-li nová syntaxe sloužit jako programovací jazyk spravovaných systémů, musí poskytovat některé prostředky, umožňující (statický) downcast v době kompilace. To je důvod, proč je uplatňování zápisu static_cast povoleno pro zachování downcastu v čase kompilace:

// ok: cast performed at compile-time. 
// No run-time check for type correctness
static_cast< array<ItemVerb^>^>(verbList->ToArray(ItemVerb::typeid));

Problém je, že neexistuje žádný způsob jak zaručit, že programátorem vyvolaný static_cast je správný a dobře míněný; to znamená, že neexistuje způsob, jak přinutit spravovaný kód být ověřitelný. To je naléhavějším zájmem v paradigmatu dynamického programování než nativního, ale není dostatečné v rámci programovacího jazyka systému nepovolit uživateli možnost přepnout mezi přetypováním statickým a za běhu.

V nové syntaxi je však výkonová past a nástraha. V nativním programování není žádný rozdíl ve výkonu mezi zápisem přetypování starým stylem a novým stylem zápisu static_cast. Ale v nové syntaxi je zápis přetypování starým stylem výrazně dražší než použití nového stylu zápisu static_cast. Důvodem je, že kompilátor interně transformuje použití starého zápisu na kontrolu za běhu, která vyvolává výjimku. Kromě toho toto také změní profil spuštění kódu, protože to způsobuje, že nezachycená výjimka shazuje aplikaci – možná uvážlivě, ale stejná chyba by neměla způsobit tuto výjimku, pokud bude použit zápis static_cast. Někdo může uvádět, že to pomůže produkčním uživatelům v používání nového stylu zápisu. Ale pouze v případě, že selže; jinak to způsobí, že programy používající starý styl zápisu poběží výrazně pomaleji bez viditelné znalosti příčiny, podobně jako následující nástrahy programátorů v jazyce C:

// pitfall # 1: 
// initialization can remove a temporary class object, 
// assignment cannot
Matrix m;
m = another_matrix;

// pitfall # 2: declaration of class objects far from their use
Matrix m( 2000, 2000 ), n( 2000, 2000 );
if ( ! mumble ) return;

Viz také

Odkaz

C-Style Casts with /clr

safe_cast

Koncepty

Obecné jazykové změny