Kurz C++ (19.)

Vítejte u dalšího dílu, který bude věnován prostorům jmen a speciálním operátorům, které nám mohou pomoci při přetypování.

19.1. Prostory jmen

    Minule jsme si psali o výjimkách, kde jsme se seznámili s prostory jmen. Konkrétně s prostorem jmen std, do něhož spadají funkce ze standardní knihovny jazyka C++. Prostory jmen umožňují rozdělit identifikátory, tedy např. proměnné nebo funkce, do velkých skupin. Tyto skupiny jsou "odstíněné", tedy v jednom prostoru jmen může být identifikátor a, který představuje např. proměnnou,  v jiném pak identifikátor a, který může představovat funkci. Za chvíli uvidíte, že k identifikátorům definovaným v prostoru jmen přistupujeme podobně, jako jsme přistupovali k statickým proměnným definovaným uvnitř třídy.

    Prostor jmen deklarujeme následujícím způsobem:

namespace JmenoProstoru { Deklarace_prvku_prostoru }

19.1.1. Pojmenované prostory jmen

    JmenoProstoru nahradíme zvoleným názvem. Můžeme se však rozhodnout tento identifikátor vynechat, pak dostaneme tzv. anonymní prostor jmen. Deklarace_prvku_prostoru jsou pak klasické deklarace funkcí a proměnných po vzoru globálních proměnných. Deklarace prostoru jmen může být rozdělena i do více částí (části dokonce nemusí ležet pouze v jednom souboru):

namespace Test { int i; }

namespace Test { float f; }

    Nyní obě proměnné leží ve stejném prostoru jmen s názvem Test. Pokud definujeme dva prostory jmen, které jsou vzájemně provázány, pak musíme dodržet pravidla, se kterými jsme se setkali už u tříd. Mějme prostory jmen Test1 a Test2 podle následujícího kódu:

namespace Test1
{
    int i;
    Test1() { Test2::f = 5.0f; }
}

namespace Test2 { float f; }

    Při pokusu o překlad nám překladač ohlásí chybu, protože nezná ani prostor jmen Test2 a už vůbec ne proměnnou f. Tento problém se dá vyřešit prohozením obou deklarací. Na tomto úseku kódu také vidíme, jak se přistupuje k prvkům uvnitř prostoru jmen - pomocí operátoru čtyřtečky ::. Za chvíli si ukážeme ještě jiný způsob, který nám ušetří velké množství napsaného kódu.

    Pokud bychom zapisovali těla funkcí přímo do deklarace prostoru jmen, deklarace by se velmi brzo stala nepřehlednou. Proto podobně jako u tříd lze těla funkcí umístit mimo:

namespace Matika
{
    int chyba;   
// Nastala naposledy chyba ?
    int NaDruhou(int a);
}

int Matika::NaDruhou(int a)
{
    chyba = 0;
// Nenastala chyba
    return a*a;
}

    Proměnná chyba ve funkci Odmocni() odpovídá proměnné chyba z prostoru jmen Matika. Pokud by existovala globální proměnná s názvem chyba, pak je zastíněna v těle funkcí náležejících do tohoto prostoru proměnnou z prostoru jmen Matika. Pokud bychom chtěli použít z nějaké funkce takovouto globální proměnnou, pak musíme použít operátoru čtyřtečka ::, tedy např. ::chyba.

    Nyní si povíme o druhém způsobu, jak si zpřístupnit identifikátory nějakého prostoru jmen. Ukážeme si to na rozsáhlém prostoru std. Zkusíme si následující příklad:

#include <iostream> // tady opravdu neni .h

int main(int argc, char* argv[])
{
    std::cout << "Nazdar!!!" << std::endl;
    return 0;
}

    Podle standardu jazyka C++ bychom měli hlavičkové soubory C++ psát bez přípony .h (stdio.h, conio.h a další soubory jazyka C zůstávají s příponou). Potom však musíme všechny identifikátory uvádět i s názvem prostoru jmen, do kterého náleží, jak si můžete všimnout u cout a endl. Jistě by bylo velice namáhavé používat všude místo jednoduchého zápisu cout std::cout. Proto existuje direktiva using, kterou si zpřístupníme celý prostor jmen. Stačí ji uvést  za řádek s direktivou #include v následující podobě:

using namespace std;

    Kromě direktivy using existuje i deklarace stejného jména, ale ta zpřístupní pouze jeden zvolený identifikátor z nějakého prostoru jmen. V případě, kdy víme, že nebudeme potřebovat veškeré identifikátory z prostoru jmen (obzvláště rozsáhlého prostoru), je použití deklarace using vhodnější. Následuje výše uvedený program, ale jsou zpřístupněny jen dva identifikátory:

#include <iostream> // tady opravdu neni .h

using std::cout;
using std::endl;

int main(int argc, char* argv[])
{
    cout << "Nazdar!!!" << endl;
    return 0;
}

    Vidíme, že pro každý identifikátor, který chceme zpřístupnit, musíme napsat jednu deklaraci using. U zpřístupňovaného identifikátoru se neuvádí typ proměnné, návratový typ funkce a neuvádějí se ani závorky po funkci. Pokud bychom tedy chtěli zpřístupnit funkci NaDruhou() z prostoru Matika, uvedli bychom:

using Matika::NaDruhou;    // ne NaDruhou()

    Doposud jsme se setkávali např. s proudy pro vstup cin a výstup cout, které také patří do prostoru std. Ale my jsme je volali přímo, nemuseli jsme před ně vkládat std::. To bylo způsobeno tím, že pokud překladač zjistí, že používáme soubory s příponou .h, pak direktivu using doplní sám. Možná se ptáte, proč byste tedy měli používat v direktivě #include jméno bez přípony. Odpověď najdete např. v nejnovější verzi vývojového prostředí Microsoft Visual Studio .NET. Pokud vložíte soubor iostream.h, překladač kód sice přeloží, ale s upozorněním, že tento soubor již nebude v další verzi vývojového prostředí k dispozici a máte tedy používat soubor iostream.

    Je třeba dát pozor, že zpřístupněním nějakého prostoru mohou překladači vzniknout nejasnosti jako v následující ukázce:

namespace Prostor
{
    int test;
    int Test() { return test; }
}

class Test {
    int a;
};

using namespace Prostor;

int main(int argc, char* argv[])
{
    Test test;   
// problem dela funkce Test() a trida Test
    return 0;
}

    Kromě proměnných a funkcí se může v deklaraci prostoru jmen objevit další prostor jmen. Potom se jedná o tzv. vnořený prostor jmen. Pokud chceme přistupovat k prvkům z vnořeného prostoru, musíme je uvádět plným jménem, tedy včetně prostoru nadřazeného. Příklad:

namespace Prostor
{
    namespace PodProstor {
        int test;
        int Test() { return test; }
    }

    int test;
    int NaDruhou(int a) { return a*a; }
}

int main(int argc, char* argv[])
{
    Prostor::PodProstor::Test();
    return 0;
}

    Pokud bychom potřebovali do prostoru PodProstor vložit další vnořený prostor, pak přístup k jeho prvkům by byl velice nepříjemný pro zápis. Jazyk C++ nabízí tedy "přezdívky" (aliasy), které pak umožňují kratší zápis:

namespace PP = Prostor::PodProstor;

    Po tomto řádku můžeme přistupovat k funkci Test náležející do prostoru jmen PodProstor pomocí následujícího zápisu:

PP::Test();

    Samozřejmě, že nám nic nebrání v použití direktivy using, kterou si můžeme zpřístupnit celý PodProstor:

using namespace Prostor::PodProstor;

    V objektových typech (třídě a struktuře) nelze použít direktivu using ke zpřístupnění celého prostoru jmen, ale lze tam použít deklaraci using, pomocí které jsme upravovali v dílu 14 přístupová práva v objektech (vlastně jsme zpřístupnili prvek z předka).

19.1.2. Anonymní prostory jmen

    Doposud jsme mluvili o prostorech, kterým jsme přidělili jméno. Pokud se rozhodneme jméno nepřidělit, vzniká tzv. anonymní prostor. Chceme-li zavolat funkci definovanou v tomto prostoru, použijeme pouze její jméno podobně jako by to byla globální funkce. Stejný postup pak platí i pro ostatní identifikátory patřící do tohoto prostoru.

    Při překladu jsou všechny anonymní prostory spojeny v jeden, kterému je navíc přiděleno unikátní jméno (liší se mezi různými soubory). Tím pádem proměnné a funkce z prostoru jednoho souboru nelze použít v souboru jiném. Je to jako bychom je prohlásili za statické (přístupné jen v modulu, kde byly definovány).

19.2. Operátory přetypování

    V minulých dílech jsme se setkali s přetypováním v následující podobě:

(typ)JmenoPromenne

    Jazyk C++ nám však nabízí ještě čtyři speciální operátory - static_cast, dynamic_cast, const_cast a reinterpret_cast. Tyto operátory byly zavedeny, aby odstranili některé nejednoznačnosti při typové konverzi. Následující kód je platný:

class A {
    int m_iTmp;
};

class B {
private:
    int m_b;
public:
    B() { m_b = 55; }
    void Fce() { return m_b*m_b; }
};

int main(int argc, char* argv[])
{
    A a;
    B *b;
    b = (B*) &a;   
// legalni pretypovani

    b->Fce();
    return 0;
}

    Ačkoliv se tento kód na většině překladačů bez problémů zkompiluje, je nesprávný. Všimněte si, že voláme metodu Fce(), ale v programu neexistuje žádný objekt typu B. Proměnná b je pouze ukazatelem na b, neexistuje tedy ani členská proměnná B::m_b. Výsledkem takovéto operace může být v lepším případě nesprávný výsledek, v horším pak chyba při běhu programu.

    Operátory se zapisují v následující podobě:

operator_pretypovani<typ>(vyraz);

    Jako operator_pretypovani uvedeme jeden z výše uvedených. Vyraz je to, co se má převést a typ určuje, co bychom chtěli dostat jako výsledek konverze.

19.2.1. Operátor static_cast

    Tento operátor pouze převede vyraz na typ a to pouze na základě typů přítomných ve výrazu vyraz. Pokud tedy máme ukazatel na třídu, pak static_cast může provést přetypování odvozené třídy na její rodičovskou třídu, ale také nám umožňuje přetypovat ukazatel na rodičovskou třídu na ukazatel na odvozenou třídu. Tento operátor neprovádí žádné ověření za běhu programu, narozdíl od níže uvedeného dynamic_cast. V prvním případě to je platné přetypování, kterého bychom mohli docílit i použitím klasického zápisu (typ), ale druhý případ zavání problémy, pokud nevíme co opravdu děláme. Operátor se používá ve spojení s typy, které nejsou polymorfní, tedy neobsahují virtuální funkce.

    Tento operátor lze také použít ke konverzím mezi základními datovými typy:

double pi=3.14159265;
int celacast = static_cast<int>(pi);

19.2.2. Operátor dynamic_cast

    Tento operátor pouze převede vyraz na typ a to pouze na základě typů přítomných ve výrazu vyraz. Typ může být pouze ukazatel nebo reference na dříve deklarovanou třídu nebo ukazatel na typ void (tedy void *). Typ výrazu vyraz závisí na typu typ. Pokud je typ ukazatelem, pak musí i vyraz být ukazatelem. Pokud je to reference, pak to musí být l-hodnota, tedy výraz který může stát i na levé straně rovnítka při přiřazení. Operátor se narozdíl od static_cast používá ke konverzím polymorfních typů.

    Jak jsme si uvedli výše, tak dynamic_cast provádí kontrolu za běhu programu, jestli konverze má smysl. Mohou nastat dva problémy:

    Ukážeme si příklady na oba případy:

#include <iostream>

using namespace std;

class Rodic { virtual Fce() { ; } };
class Potomek : public Rodic { };

int main(int argc, char* argv[])
{
    Rodic* r1 = new Potomek;
    Rodic* r2 = new Rodic;

    Potomek* p1 = dynamic_cast<Potomek*>(r1);   
// to je v poradku
    Potomek* p2 = dynamic_cast<Potomek*>(r2);   
// tady se vrati NULL

    if(p1 == NULL)
    {
        cout << "Chyba!" << endl;
    }
    else
    {
        cout << "OK!" << endl;
    }
    if(p2 == NULL)
    {
        cout << "Failed!" << endl;
    }

    char c;
    cin >> c;
    return 0;
}

    K tomuto příkladu je nutné uvést, že ne všechny překladače mají implicitně nastavenou podporu pro dynamickou identifikaci typů. To je i případ Microsoft Visual C++ 6.0, po překladu nás překladač varuje, že se snažíme o konverzi pomocí dynamic_cast bez tohoto nastavení a po spuštění programu dojde k vyvolání výjimky. Pro nastavení RTTI (Run Time Type Information - Informace o typu za běhu programu) otevřete menu Project, zvolte Settings. V okně Settings pak vyberte kartu C/C++ a v listboxu Category vyberte C++ Language. Zaškrtněte volbu Enable RTTI. Program by pak měl fungovat správně.

19.2.3. Operátor const_cast

    Jedinou funkcí tohoto operátoru je, že modifikuje atributy const, volatile a _unaligned, což žádný jiný přetypovávací operátor nedokáže. Může tyto atributy buď přidávat nebo ubírat:

class A { /* deklarace */ };
const A *a = new A;
A *b = const_cast<A*> (a);

19.2.4. Operátor reinterpret_cast

    Tento operátor převede ukazatel jakéhokoliv typu na jakýkoliv jiný typ. Navíc lze pomocí něj převést libovolnou celočíselnou hodnotu na ukazatel, popř. obráceně. Je jasné, že takovéto operace jsou velice nebezpečné a často nepřenositelné. Rozumným použitím je převod ukazatele na jiný a pozdější konverze zpět na původní typ. Ostatní konverze jsou přinejmenším nepřenositelné, v tom horším pak nebezpečné, jak jsme si uvedli.

class A {};
class B {};
A *a = new A;
B *b = reinterpret_cast<B*>(a);    // ackoliv spolu tridy vubec nesouvisi, lze to pouzit

19.2.5. Upozornění

    Operátory reinterpret_cast a const_cast by se měly používat pouze, pokud jiné řešení opravdu není možné. Umožňují totiž konverze, které představují stejné nebezpečí jako původní operátor přetypování (příklad byl na začátku odstavce o operátorech přetypování).
 

19.3. Co bude příště?

    Příště si povíme něco o určení typu za běhu programu a pak se budeme věnovat vstupním a výstupním proudům. Tím bude prozatím přerušen kurz o C++, protože si uděláme výlet do světa datových struktur, bez nichž žádný smysluplný program nemůže existovat. Potom se vrátíme ještě na chvíli ke kurzu C++, konkrétně k STL (Standard Template Library), což je knihovna šablon jazyka C++ usnadňující práci s některými datovými strukturami.

Příště nashledanou.

Ondřej Burišin