Jste zde

Co je nového v C++11

Někteří již možná zaregistrovali, že v sprnu loňského roku byl schválen nový standard jazyka C++: ISO C++11. Ten byl během své přípravy znám pod názvem C++0x. O tom, co je v něm nového oproti předchozímu standardu, C++98, se dozvíte v tomto velmi dlouhém příspěvku :).

Poznámka na začátek: Tento příspěvek koresponduje k mé prezentaci na interním semináři projektu Lissom (sekce rekonfigurovatelného zpětného překladače).

O čem si budeme povídat?

Nejdříve si stručně projdeme historii jazyků C a C++. Poté se podíváme na to, co je v C++11 nového oproti C++98, ať už z hlediska jazyka samotného tak z hlediska standardní knihovny. Jelikož je změn velmi mnoho a všechny je neprojdeme, tak bude následovat seznam všeho, k čemu jsme se nedostali. Od C++11 pak některé záležitosti z C++98 zastaraly či byly odebrány - dozvíte se, které části to jsou. No a jelikož není nad to si vše pěkně vyzkoušet, tak se na závěr dozvíte, jak je to s podporou nové normy v překladačích, standardních knihovnách a ostatních nástrojích, které pracují se zdrojovými soubory (doxygen).

Cílem je, abyste nové vlastnosti dostali do svého podvědomí. Rozhodně si nekladu za cíl důkladné seznámení se vším, co v nové normě je. Spíše se zaměřím na věci, které mě zaujaly. V mnoha případech budu výklad směřovat od příkladu v C++98 a ukázání, jak se to dá udělat v C++11.

Jdeme na to :). Začneme historií.

Stručná historie jazyků C a C++

Jelikož jsou jazyky C a C++ velmi provázané, projdeme si historii obou, a to pěkně chronologicky, aby v tom byl pořádek.

  • 1973 - C První verze jazyka C, kterou vytvořil Dennis Ritchie. Na jeho vývoji se během let 1969 až 1973 podíleli i další známí lidé, např. Brian Kernighan a Ken Thompson.
  • 1978 - K&R C Před tím, než byl jazyk C vůbec standardizován, tak bylo za "standard" považováno tzv. "C podle Kernighana a Ritchieho", které bylo popsáno v prvním vydání knihy The C Programming Language. Mezi vlastnosti, kterou v té době jazyk C měl, patřil z dnešního pohledu zvláštní způsob zápisu funkcí, kde typy parametrů byly specifikovány až za uzavírací závorkou ")" a implicitním typem byl int.
  • 1981 - C with Classes Bjarne Stroustrup zahájil od roku 1979 práci na rozšíření jazyka C o třídy a dal mu název "C s třídami". Překlad tohoto nového jazyka probíhal tak, že se zdrojové kódy nejdříve převedly do jazyka C a následně se použil Céčkový překladač.
  • 1985 - C++ V roce 1983 došlo ke změně názvu jazyka na C++, což má být hříčka využívající operátor inkrementace a má značit následníka jazyka C. V roce 1985 pak vyšla kniha The C++ Programming Language, která, obdobně jako The C Programming Language u jazyka C, sloužila jaho tehdejší jazyková reference.
  • 1990 - ISO C90 Jazyk C byl standardizován mezinárodní organizací ISO. Někteří možná ví, že v roce 1989 byl jazyk C standardizován organizací ANSI provádějící standardizaci pro Severní Ameriku. Tomuto standardu se říká ANSI C89. ISO tento standard převzalo, pouze provedlo drobné změny, především týkající se formátování. Proto se normy ISO C90 a ANSI C89 dají považovat za totožné. Standardizace pomohla rozšíření tohoto jazyka.
  • 1998 - ISO C++98 První standard jazyka C++.
  • 1999 - ISO C99 Druhý standard jazyka C. Mezi nové vlastnosti patří např. inlining funkcí, typ long long, komplexní čísla, pole na zásobníku, jehož délka je určena až za běhu programu, a zavedení řádkového komentáře (//). Jako zajímavost lze uvést, že standardy C99 a C++98 vznikaly poměrně odděleně a obsahují mnoho odlišností (např. v C++ chyběl až do standardu C++11 typ long long int).
  • 2003 - ISO C++03 V roce 2003 proběhla na základě tzv. "defect reportů" oprava standardu ISO C++98. Jelikož se však jednalo o detaily či se změny týkaly jen tvůrců překladačů, tak se tato oprava nijak nerozlišuje od vlastního standardu C++98.
  • 2007 - ISO C++TR1 V roce 2005 vznikl dokument Technical Report 1 (TR1), který obsahuje rozšíření standardu C++98 o vlastnosti připravované pro nový standard C++ či o vlastnosti přejaté z C99. Jak příklad lze zmínit chytré ukazatele (angl. smart pointers), ntice a hashovací tabulky. Nejedná se o standard jako takový, spíše o dokument obsahující návrh nových vlastností. Jeho účelem bylo, aby se tvůrci standardních knihoven a překladačů připravili na nástup nového standardu a sjednotili rozhraní k rozšířením oproti C++98. Mnoho překladačů totiž nabízelo např. hashovací tabulky ještě před tímto dokumentem. Pokud daný překladač poskytoval funkcionalitu z tohoto dokumentu, pak ji umístil do jmenného prostoru std::tr1.
  • 2011 - ISO C++11 Nejnovější standard jazyka C++. O něm bude řeč ve zbytku příspěvku i v případných dalších příspěvcích :).
  • 2011 - ISO C11 Jazyk C se v minulém roce taktéž dočkal revize, která ovšem, alespoň z mého pohledu, není tak velká, jako u C++11. K novinkám v této normě se možná dostanu v některém z budoucích příspěvků.

Co je nového

Poté, co jsem vás určitě dokonale unudil historií, začneme s průzkumem nového standardu. Nejdříve podíváme na nové jazykové vlastnosti a poté na novinky ve standardní knihovně.

Automatická typová inference (auto)

V C++98 je třeba u každé proměnné specifikovat její typ, i když je z kontextu jasné, o jaký typ jde. Pokud jste např. chtěli iterovat přes vektor čísel, mohli jste použít následující for cyklus:

// C++98
for (vector<int>::iterator i = v.begin(), e = v.end(); i != e; ++i) {
    process(*i);
}

Poznámka bokem: Všimněte si uložení v.end() do proměnné e v inicializační částí cyklu, aby se tato metoda nemusela volat při každém volání cyklu. Tohle je užitečný idiom pro iteraci nad kontejnery.

Z kontextu je jasné, co bude typem proměnných i a e. C++11 umožňuje se této explicitní specifikaci typu vyhnout za použití klíčového slova auto:

// C++11
for (auto i = v.begin(), e = v.end(); i != e; ++i) {
    process(*i);
}

Jako další příklad lze zmínit vyhledání prvku v std::map<>:

// C++98
std::map<std::string, int>::iterator pos = m.find(target);

V C++11 stačí napsat:

// C++11
auto pos = m.find(target);

což je mnohem přehlednější.

Jako poznámku na závěr bych zmínil, že klíčové slovo auto je v jazycích C a C++ už od jejich počátku, ovšem mimo C++11 má odlišný význam: jedná se o specifikátor paměťové třídy pro lokální proměnné. Jelikož je však tato paměťová třída u lokálních proměnných implicitní, je jeho použití zbytečné. Pokud by vás zajímalo využití tohoto klíčového slova ve starších verzích jazyka C, mrkněte zde.

Odkazy: 0, 1, 2, 3, 4, 5

Nová verze cyklu for

Uvažujme ještě jednou onen cyklus z předcházející části o typové inferenci:

// C++98
for (vector<int>::iterator i = v.begin(), e = v.end(); i != e; ++i) {
    process(*i);
}

C++11 nabízí další typ for cyklu, známý v jiných jazycích pod názvem foreach cyklus. Pomocí něho lze velmi snadno iterovat nad všemi prvky daného kontejneru:

// C++1
for (int& x : v) {
    process(x);
}

Stačí, aby daný kontejner poskytoval metody begin() a end().

Mezi další výhody tohoto typu cyklu patří to, že pomocí něho lze iterovat i přes klasická Céčková pole:

int numbers[] = {1, 2, 3, 4, 5};
 
// C++11
for (int& x : numbers) {
    x *= 2;
}

Všimněte si použití reference (pokud bychom referenci nepoužili, tak by se hodnoty prvků v poli nezměnily). V C++98 bychom museli iteraci provést takto:

// C++98
for (size_t i = 0, e = sizeof(numbers)/sizeof(numbers[0]); i != e; ++i) {
    numbers[i] *= 2;
}

Závěrečné poznámky:

  • Místo explicitní specifikace typu lze využít klíčové slovo auto. Onen typ proměnné by pak v obou případech byl int &.
  • Pokud chcete provést iteraci v opačném pořadí (tzv. reverse iteration), tak je potřeba trošku víc kódu.

Odkazy: 0, 1

Inicializace kontejnerů výčtem prvků

Klasické Céčkové pole lze nainicializovat výčtem prvků snadno:

// C++98
int a[] = {1, 2, 3, 4, 5};

Obtížnější to je již u kontejnerů ze standardní knihovny, kdy pokud bychom chtěli vytvořit vektor obsahující stejná čísla, jako pole výše, museli bychom udělat něco takovéhoto:

// C++98
std::vector<int> v;
v.push_back(1);
// ...
v.push_back(5);

Samozřejmě, existují i jiné varianty, ale všechny zahrnují potřebu napsání kódu navíc. V C++11 je situace mnohem snadnější. Skutečně, inicializaci vektoru lze provést takto:

// C++11
std::vector<int> v = {1, 2, 3, 4, 5};

Ani to nebolelo a máme nainicializovaný vektor :). Takovýto způsob inicializace vektoru lze využít i v případě, že voláte funkci, která očekává vektor:

// C++11
void f(std::vector<std::string> v) {
    // ...
}
f({"a", "b", "c"});

Nejlepší na tom je, že takto to funguje i u ostatních kontejnerů ze standardní knihovny, nikoliv jen u vektoru. Dále, podporu pro inicializaci tímto způsobem lze doimplementovat i do vašich tříd.

Odkazy: 0, 1, 2

Jednotná inicializace objektů

Mějme následující kus kódu:

struct BasicStruct {
    int x;
    double y;
};
 
struct ClassStruct {
    ClassStruct() {}
    ClassStruct(int x, double y): x(x), y(y) {}
    int x;
    double y;
};

Je v něm definovaná klasická Céčková struktura a poté struktura s konstruktory, která se chová jako třída. Pokud bychom chtěli vytvořit instanci BasicStruct a nainicializovat ji, můžeme použít následující syntaxi, která pochází již z dob jazyka C:

// C++98
BasicStruct a = {1, 2.5};

Při inicializaci ClassStruct musíme použít následující syntaxi (nejedná se již totiž o klasickou strukturu, ale o třídu):

// C++98
ClassStruct b(2, 8.9);

No a pokud bychom chtěli inicializovat ClassStruct pomocí jiného objektu té stejné třídy, musíme napsat:

// C++98
ClassStruct c((ClassStruct())); // ?

Zdánlivé závorky navíc jsou nutné. Zápis bez nich by totiž znamenal deklaraci funkce c, která vrací ClassStruct a bere jako parametr ukazatel na funkci bez parametrů vracející ClassStruct... Stojí za tím způsob rozlišení nejednoznačných konstrukcí v C++.

Všimněte si, že ve všech třech případech jsme museli použít jinou syntaxi. Jednou to byly složené závorky, jednou klasické, a ve třetím případě dvojité klasické závorky. C++11 toto řeší zavedením jednotného způsobu inicializace:

// C++11
BasicStruct a{1, 2.5};
ClassStruct b{2, 8.9};
ClassStruct c{ClassStruct()};

Vypadá to zvláštně, že? :) Věřte, že v C++ se budete se složenými závorkami setkávat mnohem více, než obvykle. Například třeba u anonymních funkcích, na které se mrkneme za chvíli.

Odkazy: 1

Alternativní zápis funkcí a klíčové slovo decltype

C++11 zavádí alternativní zápis funkcí. Místo

// C++98
int f(int x, int y);

lze nyní psát

// C++11
auto f(int x, int y) -> int;

Teď si asi říkáte něco ve smyslu: "Proboha, proč?" Tento nový zápis se hodí v následujících dvou situacích:

  • Využití v šablonách. Uvažujte následující šablonu funkce, která sečte dvě předané hodnoty a vrátí výsledek:
    // C++11
    template<class Lhs, class Rhs>
    auto add(const Lhs& lhs, const Rhs& rhs) -> decltype(lhs + rhs) {return lhs + rhs;}

    Nový operátor decltype vrátí typ předaného výrazu. Pokud bychom u této šablony použili klasický styl zápisu funkce, pak by překlad selhal. Důvod je ten, že na místě, kde je teď klíčové slovo auto, nemůžeme napsat decltype(lhs + rhs), protože v té době ještě překladač neví, co to je lhs a rhs.

  • Odstranění opakování. Uvažujme následující třídu, která má dlouhé jméno:
    class LongClassName {
        typedef std::vector<int> IntVec;
        IntVec f();
    }

    V ní je použit typedef pro vektor čísel a deklarována funkce f(). Nyní, když bychom měli definovat funkci f(), tak v C++98 bychom museli napsat:

    // C++98
    LongClassName::IntVec LongClassName::f() { /* ... */ }

    Všimněte si dvojnásobné kvalifikace LongClassName, která je nutná (překladač před tím, než narazí na LongClassName::f(), neví, že se budeme pohybovat v prostoru jmen dané třídy). S využitím alternativního zápisu lze první kvalifikaci vynechat:

    // C++11
    auto LongClassName::f() -> IntVec { /* ... */ }

Odkazy: 0, 1, 2

Anonymní (lambda) funkce

Anonymní, alias nepojmenované či lambda funkce, patří mezi součást mnoha vysokoúrovňových jazyků. S novou normou přichází i jejich podpora do C++.

Začneme příkladem. Anonymní funkci, která nám sečte dvě čísla, můžeme zapsat následovně.

// C++11
[](int x, int y) { return x + y; }

Hranaté závorky následované kulatými nám určují, že se bude jednat o lambda funkci. Funkce má dva parametry typu int a vrací jejich součet. Takovou funkci můžeme využít na místech, kde se očekává funkce či functor (funkční objekt).

Obecná syntaxe je následující:

[capture](parameters) -> return-type { body }

Obsah hranatých závorek nám určuje, jak se budou do funkce předávat proměnné z okolí (viz dále). Parametry jsou pak zapsány klasicky, jak jsme zvyklí. Následuje specifikace návratového typu, která se zapisuje oním novým stylem, o kterém jsme si povídali před chvílí. Pokud má tělo funkce jen jediný příkaz, který vrací hodnotu, pak se návratový typ detekuje automaticky a není třeba jej explicitně zmiňovat. Následuje tělo funkce ve složených závorkách.

Pojďme si ukázat, jak se to dá použít. Dejme tomu, že máme třídu Person, která obsahuje informace o osobě. Dále mějme vektor osob, který chceme seřadit podle jejich identifikátoru (ID, unikátní číslo v rámci organizace). K tomu využijeme standardní funkci std::sort(), které předáme rozsah, který budeme seřazovat a funkci, která se použije pro řazení:

// C++11
std::sort(people.begin(), people.end(),
        [](const Person& p1, const Person& p2) {
    return p1.getId() < p2.getId();
});

Jak si můžete všimnout, tak jsme s výhodou využili možnosti, kterou nám poskytuje C++11 a jeho lambda funkce. V opačném případě bychom si museli tuto řadící funkci vytvořit mimo tento kód, což by jej mohlo znepřehlednit.

V další ukázce se dozvíme, k čemu slouží obsah hranatých závorek. Mějme vektor čísel a označme si jej v. Naším úkolem je sečíst všechny jeho hodnoty a uložit je do proměnné total. Uděláme to např. takto:

// C++11
std::vector<int> v = // ...
int total = 0;
std::for_each(v.begin(), v.end(), [&total](int x) {
    total += x;
});

Samozřejmě by to šlo řešit i jiným způsobem - toto je jen ukázka. Opět jsme využili standardní funkce, tentokrát std::for_each(). Všimněte si, že do hranatých závorek jsme museli napsat &total, čímž si zaručíme, že se proměnná total předá do funkce referencí, nikoliv hodnotou, jak by tomu bylo, kdybychom nechali tyto závorky prázdné. Tímto způsobem se dají v C++ implementovat a použít tzv. closures.

Odkazy: 0, 1, 2

Konstanta nullptr pro nulový ukazatel

V C++98 se jako nulový ukazatel dá použít buď konstanta 0, nebo Céčkové makro NULL (je však doporučováno používat 0). Ani jeden z přístupů však není ideální. Mějme následující dvě funkce:

void f(int);
void f(int *);

Když se pokusíme zavolat f() s nulovým ukazatelem, tak dopadneme v obou případech stejně:

// C++98
f(0);    // zavolá void f(int);
f(NULL); // taktéž zavolá void f(int);

Člověk by očekával, že f(NULL) zavolá void f(int *), ale není tomu tak. Důvod je ten, že v C++ je makro NULL definováno jako celočíselná nula.

C++11 přichází s řešením, kterým je nové klíčové slovo nullptr, což je nulový ukazatel. Je typu std::nullptr_t a je implicitně konvertovatelný na jakýkoliv jiný ukazatelový typ a na bool, což se hodí např. při testech, zda ukazatel není nulový. Pro konverzi na int je třeba použít reinterpret_cast<>. Použití je následující:

// C++11
char *pc = nullptr; // OK
int  *pi = nullptr; // OK
bool   b = nullptr; // OK (b je false)
int    i = nullptr; // ! (chyba při překladu)
 
f(0);       // zavolá void f(int);
f(nullptr); // zavolá void f(int *);

Odkazy: 0, 1

Silně typované výčty

Výčty se v C++98 chovají jinak, než třídy. Uvažujme následující kód:

enum MyEnum {
    VAL1,
    VAL2,
    VAL3
};
 
int i = VAL2;

Za prvé, v C++98 je výčet skrz naskrz kompatibilní s intem, takže je lze mezi sebou libovolně převádět. Lze mezi sebou převádět a porovnávat i hodnoty různých výčtů. Za druhé, enum nevytváří prostor jmen, takže všechny jeho prvky jsou součástí jmenného prostoru, ve kterém je definován onen enum. To způsobuje problémy, pokud potřebujete více různých výčtů se stejnými konstantami. Za třetí, standard nedefinuje, co přesně za typ se má použít pro uložení výčtu, takže to může být char, int, či cokoliv jiného. Znát typ, který se použil k uložení, je výhodné v situaci, pokud chcete např. prvky onoho enumu serializovat a posílat přes síť.

C++11 tyto problémy adresuje zavedením tzv. silně typovaných výčtů:

enum class MyEnum: unsigned int {
    VAL1,
    VAL2,
    VAL3, // :)
};
 
int i = MyEnum::VAL2; // ! (chyba při překladu)

Všimněte si použití klíčového slova class. U těchto výčtů volitelně specifikovat typ, na kterém budou uloženy (implicitně int, zde unsigned int). Dále, tvoří již samostatný prostor jmen, takže na stejné úrovni může existovat více výčtů se stejně pojmenovanými konstantami. Důležitý fakt také je, že prvky výčtů již nejsou implicitně převeditelné na int ani na jakýkoliv jiný výčet. Potěší i to, že nyní čárka za posledním prvkem v enumu již není syntaktická chyba :).

Odkazy: 0, 1, 2

Explicitní redefinice

V C++98 můžete narazit na zajímavý a zákeřný problém, pokud se pokusíte redefinovat (angl. override) metodu, ale změníte její signaturu:

// C++98
class Base {
    virtual void f(int);
};
 
class Derived: public Base {
    virtual void f(double); // skryje Base::f()
};

Ve výše uvedeném kódu je ten problém, že místo redefinice metody f() z nadtřídy dojde k jejímu překrytí novou metodou, která má parametr typu double. To vám později může způsobit bolesti hlavy při ladění. Sice si můžete říct, že vám se to nikdy nestane, ale co když se majitel bázové třídy rozhodne, že změní signaturu některé z virtuálních metod a vy si toho nevšimnete?

V C++11 se tomu dá vyhnout uvedením identifikátoru override za hlavičku oné metody, u které si chceme být jisti, že nám redefinuje již existující metodu v nadtřídě:

// C++11
class Base {
    virtual void f(int);
};
 
class Derived: public Base {
    virtual void f(double) override; // ! (chyba při překladu)
};

Při překladu dojde k chybě, protože se snažíme předefinovat metodu void f(int) pomocí void f(double).

Za pozornost stojí fakt, že se nejedná o klíčové slovo, nýbrž pouze identifikátor, který dostane speciální význam až v tomto kontextu. Můžete si tak klidně vytvořit proměnnou pojmenovanou override, což by nebylo možné, pokud by override bylo klíčové slovo.

Odkazy: 0, 1, 2, 3

Zamezení dědění tříd (final)

Mějme třídu NotBase, u které chceme, aby ji nikdo nepoužíval jako bázovou třídu, tj. chceme zakázat dědění od této třídy:

// C++98
class NotBase {};
 
class Derived: public NotBase {}; // Toto nechceme povolit.

V C++98 toto lze vynutit třemi způsoby: (1) napsáním komentáře k bázové třídě, že si nepřejeme, aby ji někdo dědil. To nám ale nepomůže proti ostrým hochům, kteří komentáře ignorují. Dále (2) tím, že konstruktor uděláme privátní a poskytneme statickou vytvářecí funkci, a (3) velmi technickým způsobem, který využívá virtuální dědičnost.

V C++11 je situace jednodušší:

// C++11
class NotBase final {};
 
class Derived: public NotBase {}; // ! (chyba při překladu)

Za název třídy se dá identifikátor final, který zaručí, že od třídy nepůjde dědit. Opět se nejedná o klíčové slovo, pouze o identifikátor.

Odkazy: 1, 2

Zamezení redefinice virtuálních metod (final)

Obdobný problém nastává v C++98, pokud chceme zaručit, že zvolenou virtuální metodu již nikdo nebude moct redefinovat (angl. override). Můžete namítat, že pak stačí udělat tuto metodu jako nevirtuální či označit celou třídu jako finální (viz výše), ale co když se jedná o jednu z podtříd, u které chceme umožnit dědědí i redefinici jiných metod, jen ne jedné konkrétní? V C++98 toto není možné.

Zde nastupuje C++11, kde s využitím identifikátoru final lze zamezit možnosti jejího redefinování v podtřídách:

// C++11
class Base {
    virtual void f();
};
 
class Derived {
    virtual void f() final;
};
 
class MoreDerived : public Derived {
    virtual void f(); // ! (chyba při překladu)
};

Opět se jedná pouze o identifikátor, nikoliv o klíčové slovo.

Odkazy: 1, 2

Explicitně zrušené metody

Častou situací v C++ je, že chceme zaručit, aby naše třída nebyla kopírovatelná (např. pro implementaci tzv. reference objects). V C++98 se toho dalo dosáhnout tak, že se zprivatizoval kopírovací konstruktor a operátor přiřazení a vynechala se jejich definice:

// C++98
class NonCopyable {
private:
    NonCopyable(const NonCopyable&); // bez definice
    NonCopyable& operator=(const NonCopyable&); // bez definice
};

Jelikož je kopírovací konstruktor a operátor přiřazení privátní, nelze je použít zvenku, a pokus o kopii instance třídy NonCopyable skončí s chybou při překladu. Vynechání definice těchto metod má za následek, že instance nepůjde kopírovat ani uvnitř jiných metod v této třídě.

V C++11 je situace o něco jednodušší:

// C++11
class NonCopyable {
public:
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
}

Obě metody mohou zůstat veřejné, jen se za jejich deklarací uvede klíčové slovo delete, které způsobí, že metody nepůjdou použít. Jejich použití totiž způsobí chybu při překladu.

Odkazy: 0, 1, 2

Typ long long int

V C++11 se objevuje typ long long int, který byl poprvé zaveden v C99. Důvodem jeho přidání bylo, aby byl i v C++ typ, který má minimálně 64 bitů. V C90 a C++98 byl totiž největší typ long int, jehož velikost byla zaručeně alespoň taková, jako velikost intu, a musel být minimálně 32 bitový.

U long long int je normou zaručeno, že je minimálně tak velký, jako long int a že jeho velikost je minimálně 64 bitů.

// C++11
std::numeric_limits<long long int>::min();
// Alespoň -9223372036854775808 (2^63).
std::numeric_limits<long long int>::max();
// Alespoň 9223372036854775807 (2^63 - 1).
std::numeric_limits<unsigned long long int>::min();
// 0
std::numeric_limits<unsigned long long int>::max();
// Alespoň 18446744073709551615 (2^64 - 1).

Odkazy: 0, 1, 2

Aserce při překladu

C++98 podporuje následující dva druhý assercí:

  • Během preprocesingu. Pomocí direktiv #if a #error.
  • Za běhu. Pomocí makra assert().

Již ale neobsahuje vestavěnou podporu pro aserce za překladu. C++11 toto napravuje zavedením klíčového slova static_assert. Formát použití je obdobný klasickému assertu:

static_assert(konstantní výraz, chybová zpráva);

Následuje ukázka použití:

// C++11
template<class Integral>
Integral mod(Integral x, Integral y) {
    static_assert(std::is_integral<Integral>::value, "mod() parameter must be of an integral type");
    return x % y;
}

V této šabloně jsme použili aserci při překladu, že šablonový typ je některý z vestavěných celočíselných typů. Využíváme tam tzv. typový trait (angl. type trait) std::is_integral<>.

Pokud se pokusíme zavolat naši šablonovou funkci mod() s hodnotami typu double, dostaneme chybové hlášení:

mod(1.2, 2.2); // ! (chyba při překladu)
// In instantiation of `Integral mod(Integral, Integral) [with Integral = double]':
// error: static assertion failed: mod() parameter must be of an integral type

V Céčku či dřívějších verzích se musely aserce při překladu řešit oklikou. Jednu z nich budu prezentovat na příkladu. Mějme kus kódu, kde si chceme při překladu ověřit, že long int má alespoň 64 bitů. Lze to udělat následovně (jak v C, tak v C++):

// C99, C++98
int StaticAssertFailed_ExpectedLongToBeAtLeast64b[(sizeof(long) >= 8) ? 1 : -1];

Pokud platí, že long int má alespoň 64 bitů, tak se vytvoří pole o jednom prvku. Pokud toto ale neplatí, tak dojde k pokusu o vytvoření pole o záporném počtu prvků, což žádná z norem nedovoluje. Výsledkem pak bude chybové hlášení podobné tomuto:

// error: size of array `StaticAssertFailed_ExpectedLongToBeAtLeast64b' is negative

Z něj je vidět, proč jsme zvolili takové "šílené" jméno proměnné. Kromě tohoto přístupu existuje i celá řada jiných přístupů, ale žádný z nich není tak čistý, jako static_assert v C++11.

Odkazy: 0, 1

R-hodnotové reference

C++11 zavádí kromě klasických referencí tzv. r-hodnotové reference (angl. rvalue references), které se dají navázat na dočasné objekty. Syntaktická odlišenost je v tom, že se použije jeden ampersand navíc. Čili, && T značí r-hodnotovou referenci na typ T. Využití r-hodnotových referencí je především pro urychlení běhu programu pomocí možnosti přesunu objektů na místo jejich kopírování, čemuž se říká anglicky move semantics a ukážeme si to dále na příkladu. Dalšímu z využití, perfect forwarding, se zde věnovat nebudeme.

Dejme tomu, že chcete v C++98 nadefinovat šablonu swap<> pro prohození dvou objektů. Standardní možnost, jak ji naimplementovat, je následující:

// C++98
template <class T> swap(T& a, T& b) {
    T tmp(a); // zkopíruje a
    a = b;    // zkopíruje b
    b = tmp;  // zkopíruje tmp
}

Problém této implementace je, že se vytváří zbytečné kopie. Vše, co chceme, je přesun dvou objektů - např. na řádku T tmp(a); nepotřebujeme vytvoření kopie, protože v zápětí na dalším řádku do proměnné a zkopírujeme b.

V C++11 to lze vyřešit za pomocí r-hodnotových referencí a standardní funkce std::move(), která nám umožní napsat následující variantu:

// C++11
template <class T> swap(T& a, T& b) {
    T tmp(std::move(a)); // bez kopie
    a = std::move(b);    // bez kopie
    b = std::move(tmp);  // bez kopie
}

Zde se nevytvoří ani jedna kopie, pouze se přesouvají "vnitřnosti" objektů. Samozřejmě, předané objekty musí takový přesun vnitřností podporovat.

Za r-hodnotovými referencemi je toho mnohem víc, takže určitě mrkněte na odkazy níže.

Odkazy: 0, 1, 2, 3, 4

Zobecněné konstantní výrazy

Mějme následující řádek kódu v C++98:

// C++98
const double d = sqrt(5.6);

K vyčíslení odmocniny z 5.6 dojde až za běhu programu. Důvodem je, že překladač není povinen dělat vyhodnocení volání funkcí za překladu, a navíc v některých situacích ani nemůže, protože nemá dostatek informací od programátora či mu to norma jazyka zakazuje. Můžete namítat, že stačí, aby vyčíslení provedl programátor a dal do kódu přímo výsledek, ale to může vyústit ve sníženou čitelnost. Ideální by bylo, kdyby se podařilo, aby podobná volání dokázal vyčíslit přímo překladač.

Obdobná situace nastává, když si chcete nadefinovat pole, jehož rozměr je dán voláním funkce, byť vracející vždy stejný výsledek:

// C++98
int f() { return 2; }
 
int arr[f()]; // ! (chyba při překladu)

Volání funkce totiž není považováno za konstantní výraz, i kdyby funkce vždy vracela stejný výsledek.

C++11 oba případy řeší přidáním klíčového slova constexpr, které značí, že daná funkce vrací konstantní výraz. Pokud tedy vhodně nadefinujeme funkci sqrt, např. takto:

// C++11
constexpr double abs(double x) {
    return (x > 0.0 ? x : -x);
}
 
constexpr double sqrtImpl(double y, double x1,
        double x2, double eps) {
    return (abs(x1 - x2) < eps ? x2 :
        sqrtImpl(y, x2, (x2 + y / x2) * 0.5, eps));
}
 
constexpr double sqrt(double y) {
    return sqrtImpl(y, 0, y * 0.5 + 1.0, 1e-10);
}

pak v následujícím případě

// C++11
const double d = sqrt(5.6); // vyhodnoceno při překladu

dojde k vyhodnocení volání za překladu, takže se tím program nemusí zdržovat za běhu. Samozřejmě, že u takovéhoto jednoduchého volání to moc smysl nedává, ale pokud by se něco počítalo v cyklu či rekurzi, tak to může značně urychlit běh programu.

U příkladu s polem je situace obdobná - stačí nadefinovat f() jako constexpr funkci:

// C++11
constexpr int f() { return 2; }
 
int arr[f()]; // OK

Překladač pak již nekřičí a kód je korektní dle normy C++11.

Na závěr bych zmínil, že pomocí tzv. template meta-programming, constexpr a využití toho, že šablony v C++ jsou výpočetně úplné, lze dělat opravdu psí kusy :). Lze dojít k zajímavým situacím, např. že překlad kódu, který má cca 13 řádek, bude trvat i 20 hodin.

Odkazy: 0, 1, 2, 3

Nyní se zaměříme na novinky ze světa šablon.

Dvojité úhlové závorky u šablon

Pokud jste chtěli v C++98 vytvořit šablonu, jejíž šablonový typ je šablona, tak jste narazil na menší lexikální nepříjemnost, a to tu, že mezi úhlovými závorkami ukončujícími zápis musela být ponechána mezera:

// C++98
std::vector<std::vector<int>> // !
std::vector<std::vector<int> > // OK

Překladač totiž dvě ukončující úhlové závorky za sebou bere jako operátor bitového posunu doprava, což je většinou nežádoucí token a má za následek syntaktickou chybu. Jinak, než použitím mezery navíc, to vyřešit nešlo.

V C++11 odpadá nutnost použít onu otravnou mezeru navíc:

// C++11
std::vector<std::vector<int>> // OK

Hurá :). Pokud by i přesto někdo potřeboval původní chování, tak se dá vynutit použitím klasických závorek.

Odkazy: 0, 1, 2

Aliasy na šablony

Dejme tomu, že byste si chtěli vytvořit slovník, tj. mapování řetězce na něco jiného, co bude parametrizované. Mohlo by vás napadnou využít std::map<> a typedef, ovšem něco takového neprojde přes překladač:

// C++98
template <typename T>
typedef std::map<std::string, T> Dictionary; // ! (chyba při překladu)
 
// Takto byste to chtěli použít:
// Dictionary<int> d;

V C++11 se na tom, že tato konstrukce je neplatná, nic nemění. C++11 však zavádí novou syntaxi, pomocí které můžeme dosáhnout kýženého výsledku:

// C++11
template <typename T>
using Dictionary = std::map<std::string, T>; // OK
 
Dictionary<int> d;

Tuto syntaxi lze používat na místo typedef nejen v případě šablon:

typedef std::ios_base::fmtflags flags; // stará syntaxe
using flags = std::ios_base::fmtflags; // nová syntaxe
 
typedef void (*func)(int, int);  // stará syntaxe
using func = void (*)(int, int); // nová syntaxe

Odkazy: 0, 1, 2, 3, 4, 5, 6, 7

Šablony s proměnným počtem typových parametrů

Dejme tomu, že chcete v C++98 napsat funkci, která Vám vytiskne to, co ji předáte, ať už jí předáte dva argumenty či deset argumentů. Použití funkcí s proměnným počtem parametrů není to pravé oříškové, protože ty vyžadují alespoň jeden fixní parametr, podle kterého se zjistí, kolik argumentů se má zpracovat a jaké mají typy. Jedna z možností je následující:

// C++98
template <typename T1>
void print(const T1& val1) {
    std::cout << val1 << "\n";
}
 
template <typename T1, typename T2>
void print(const T1& val1, const T2& val2) {
    std::cout << val1;
    print(val2);
}
 
template <typename T1, typename T2, typename T3>
void print(const T1& val1, const T2& val2, const T3& val3) {
    std::cout << val1;
    print(val2, val3);
}
 
// ...
 
print("I am ", 26, " years old."); // I am 26 years old.

Samozřejmě, když chcete podporovat až deset parametrů, musíte si udělat přetížené verze pro čtyři, pět, ..., až deset parametrů. To je velmi náročné na psaní i na údržbu, nehledě na to, že když si někdo bude chtít vytisknout jedenáct objektů, bude mít smůlu.

C++11 řeší tento problém zavedením šablon s proměnným počtem parametrů. Ona funkce print() by v C++11 mohla vypadat takto:

// C++11
template <typename T>
void print(const T& value) {
    std::cout << value << "\n";
}
 
template <typename U, typename... T>
void print(const U& head, const T&... tail) {
    std::cout << head;
    print(tail...);
}
 
print("I am ", 26, " years old."); // I am 26 years old.

Všimněte si využití rekurze, které připomíná zpracování seznamů v Haskellu či Prologu.

Šablony s proměnným počtem parametrů se hodí v mnoha případech. Jedním z případů jsou ntice (viz dále).

Odkazy: 0, 1, 2, 3, 4

Nyní přejdeme na některé novinky ze standardní knihovny.

Ntice

První novinkou ve standardní knihovně C++11 jsou ntice. Jedná se o jednodušší obdobu struktur, k jejímž prvkům se přistupuje pomocí indexů. Pokud máte ntici t, tak k Ntému prvku se dostanete pomocí get<N>(t).

Příklad:

std::tuple<std::string, int> getAddressAndPort(const std::string& url) {
    // zpracování url a získání adresy a portu
    // ...
    return std::make_tuple(address, port);
}
 
std::string host;
int port;
std::tie(host, port) = getAddressAndPort("127.0.0.1:631");

V tomto příkladu máme funkci, která nám z předaného URL získá adresu a port (příklad je to dost umělý, ale je na něm vidět princip). Jelikož tato funkce má vracet více hodnot, můžeme využít ntic. Ntice se dá snadno vytvořit pomocí funkce std::make_tuple(), u které není třeba předávat typové parametry (jedná se ve skutečnosti o šablonu). Ke snadnému uložení výsledku do dvou proměnných pak lze využít standardní funkci std::tie(), která nám naváže prvky ntice na naše proměnné.

Využití ntic:

  • Funkce vracející více hodnot. Toto využití jsme viděli v posledním příkladu.
  • Výměna hodnot. Pokud máte více hodnot, které chcete určitým způsobem vyměnit, lze to udělat takovouto fintou :):
    // C++11
    std::tie(a, b, c) = std::make_tuple(b, c, a);

Odkazy: 1, 2, 3, 4, 5

Hashovací tabulky

Další novinkou jsou implementace standardních kontejnerů pro množiny a mapy za pomocí hashovacích tabulek. U operací kontejnerů std::map<> a std::set<> totiž standard vyžaduje jisté vlastnosti z hlediska časové složitosti, které je velmi těžké splnit bez použití jiné, než stromové reprezentace. Tyto kontejnery tak bývají implementovány pomocí některé varianty AVL stromu.

Verze standardních kontejnerů, které jsou implementovány pomocí hashovacích tabulek, lze nalézt v hlavičkovém souboru <unordered_{set,multiset,map,multimap}>. Tento název byl zvolen z toho důvodu, aby nedošlo ke kolizi s již existujícími implementacemi hashovacích tabulek, které překladače běžně nabízí. Klíče u těchto kontejnerů musí být hashovatelné a porovnatelné mezi sebou. To, že musí být hashovatelné, je zřejmé. To, že musí být i porovnatelné, vyplývá z toho, že na jeden klíč se může namapovat více položek, a my potřebujeme zjistit, zda některá z položek, které mají stejný hash, je ta, kterou chceme vkládat/vyhledávat. Rozhraní je obdobné, jako mají jejich nehashovací protějšky.

Pojďme se podívat na srovnání časové složitosti operací nad std::set (stromová implementace) a std::unordered_set (implementace hashovací tabulkou); n značí počet prvků v kontejneru. V případě std::set trvají operace vložení, přidání, vymazání a vyhledání O(log(n)) (logaritmická složitost), kdežto v případě std::unordered_set pouze O(1) (konstantní časová složitost). Kde je háček? Ten je v tom, že u std::unordered_set se jedná o složitost v průměrném případě. V nejhorším případě, pokud se všechny prvky mapují na to stejné místo, je složitost O(n), což je linární (horší než logaritmická).

Odkazy: 1, 2

Chytré ukazatele

Klasické Céčkové ukazatele (angl. raw pointers) mají mimo svých nezanedbatelných výhod (např. rychlost) řadu neduh:

  • Není jisté, kdo vlastní alokovanou paměť a kdo má ukazatel uvolnit.
  • Není jasné, kdy by se měla alokovaná paměť uvolnit.
  • Alokovaná paměť musí být dealokovaná manuálně.

C++11 se tyto neduhy snaží řešit pomocí tzv. chytrých ukazatelů (angl. smart pointers). Vezměme si následující příklad, který využívá jeden z chytrých ukazatelů dostupných v C++11:

class CarFactory {
public:
    static std::unique_ptr<Car> create(/* parametery */);
}

Výše uvedená továrna podle předaných parametrů vytvoří auto a vrátí na něj ukazatel. Tímto kódem říkáme dvě věci: (1) ten, kdo funkci zavolá, se stává jediným vlastníkem ukazatele a musí se o něj postarat (tj. ukazatel není uložen a používán nikde jinde) a (2) po konci bloku, kde je ukazatel vytvořen, dojde k automatickému uvolnění paměti, na který ukazuje.

Celkem jsou v C++11 k dispozici čtyři chytré ukazatele, kde každý z nich má jiné použití a vlastnosti:

  • std::unique_ptr - jedinečný ukazatel na objekt. Není kopírovatelný, pouze přesouvatelný.
  • std::shared_ptr - sdílený ukazatel.
  • std::weak_ptr - "slabší" obdoba sdíleného ukazatele, jehož jedním z úkolů je řešit problémy s cyklickými závislostmi (chytré ukazatele v C++11 totiž využívají tzv. reference counting, kterému dělají problémy cykly, kdy na sebe cyklicky navzájem ukazuje více objektů).
  • std::auto_ptr - přežitek z C++98. Od C++11 je označen jako zastaralý (angl. deprecated). Místo něj byste měli používat std::unique_ptr (std::auto_ptr se totiž nedá uchovávat v kontejnerech).

Odkazy: 1, 2

Regulární výrazy

Poslední novinka, kterou si zmíníme, je podpora pro regulární výrazy. Regulární výrazy v C++11 jsou založeny na regulárních výrazech v Boost.regex a podporu pro ně lze nalézt v hlavičkovém souboru <regex>. Je podporováno šest různých druhů syntaxe, z nichž implicitní je ECMAScript syntaxe (podobá se syntaxi regulárních výrazů v Perlu).

V C++11 jsou k dispozici následující algoritmy:

  • std::regex_match() - zjištění, zda předaný řetězec přesně odpovídá zadanému regulárnímu výrazu
  • std::regex_search() - vyhledávání zadaného regulárního výrazu v předaném řetězci
  • std::regex_replace() - nahrazování dle zadaného regulárního výrazu

Mrkneme se na příklad. Jelikož je však podpora regulárních výrazů ve standardní knihovně na Linuxu (libstdc++) prozatím velmi mizerná, tak neručím, že bude fungovat korektně :).

// C++11
void showIPParts(const std::string& ip) {
   std::regex pattern(R"((\d{1,3}):(\d{1,3}):(\d{1,3}):(\d{1,3}))");
   std::match_results<std::string::const_iterator> result;
   bool valid = std::regex_match(ip, result, pattern);
   if (valid) {
        std::cout << result[0] << /* ... */ << "\n";
   }
}

Všimněte si použití nového řetězcového literálu, prefixovaným písmenem R. V takovém tzv. raw literálu není třeba prefixovat lomítka a uvozovky. To se obzvláště u regulárních výrazů velmi hodí. Vytvoříme si zjednodušený regulární výraz pro IP adresu, vytvoříme si pomocnou proměnnou pro uložení výsledku, provedeme matching pomocí std::regex_match() a zkontrolujeme, zda řetězec odpovídá regulárnímu výrazu. Pokud ano, pak v result máme k dispozici všechny skupiny, které jsme uvedli v regulárním výrazu. Každá skupina koresponduje k jednomu oktetu.

Odkazy: 1, 2, 3, 4, 5, 6, 7, 8, 9

Co jsme přeskočili a neprošli

Jelikož je toho v nové normě opravdu hodně, tak jsem spoustu novinek přeskočil. Níže uvádím seznam některých z nich, abyste si o nich mohli vyhledat informace na Internetu.

Aktualizace 6.10.2015: Většinu z přeskočených novinek níže jsem popsal v navazujícím příspěvku.

  • externí šablony
  • tzv. inline prostory jmen
  • modifikovaná definice POD
  • sizeof funguje na členech tříd bez nutnosti mít k dispozici instanci
  • další klíčová slova: alignof, alignas, noexcept
  • umožňuje implementace s garbage collectorem
  • podpora pro vícevláknové programování
  • unicode řetězcové literály (utf-8, utf-16, utf-32)
  • uživatelsky definované literály
  • neomezené unie
  • explicitní konverzní operátory
  • rozšířitelná podpora pro generátory náhodných čísel
  • tzv. wrapper reference
  • polymorfní wrappery pro funkční objekty
  • tzv. type traits pro šablonové metaprogramování
  • jednotná metoda pro výpočet návratového typu u funktorů
  • vylepšení konstrukce objektů
  • ... a mnoho dalších

Co z C++98 byste v C++11 neměli používat

Následující záležitost byla ze standardu odstraněna:

  • Exportované šablony. Jedná se klíčové slovo export, které umožňuje oddělit deklaraci šablony od její implementace. Když totiž definujete šablonu, tak její definice musí být přístupná všem, kteří ji chtějí používat, proto se její definice dává do hlavičkových soubor. Jak poznamenal dr. Peringer od nás z fakulty, tak toto klíčové slovo se hodí pro komerční firmy, kterým vadí "nucený open-source" jejich kódu, který poskytuje šablony :) (ve skutečnosti to tento problém tak úplně neřeší, viz detaily v uvedených článcích dále). Vešlo ve známost jako klíčové slovo, které má nejmenší podporu v překladačích (drtivá většina překladačů jej neimplementuje, včetně gcc, kvůli náročnosti implementace a/nebo kvůli tomu, že toho ve skutečnosti moc nepřináší), takže se s ním téměř nesetkáte. Pokud by vás zajímaly detaily o tom, co ve skutečnosti export znamená a jaké zde existují problémy, tak vřele doporučuji tento a tento článek od Herba Suttera.

A následující záležitosti byly zase označeny jako zastaralé (angl. deprecated):

Určitě si stojí za to přečíst tento příspěvek, kde jeho autor zmiňuje řadu idiomů, který lze v C++11 řešit elegantněji.

Podpora v překladačích, standardních knihovnách a nástrojích

Na závěr se mrkneme, jak je to s podporou nové normy v překladačích, standardních knihovnách a jiných nástrojích, které pracují se zdrojovými kódy.

Podpora v překladačích

Zde je k dispozici přehledná tabulka, která ukazuje, co z nové normy C++ je již podporováno v překladačích. Z tabulky je vidět, že nejvíce z C++11 je k dispozici v GCC a Clangu.

Podpora ve standardních knihovnách

Pokud je součástí překladače i implementace standardní knihovny, pak není co řešit, ale třeba na Linuxu překladače využívají externí standardní knihovnu. Mezi nejrozšířenější patří libstdc++, kterou využívá GCC a Clang. Podpora C++11 v této knihovně je zobrazena v této tabulce. Kromě regulárních výrazů a podpory pro vícevláknové zpracování je toho většina naimplementována.

Clang začal pracovat na své vlastní implementaci standardní knihovny. Stav této implementace je k nahlédnutí zde. Jak je vidět, dost toho chybí, takže momentálně doporučuji s Clangem používat libstdc++.

Co se týče MS windows a standardní knihovny pro Visual Studio, tak mrkněte zde.

Podpora v ostatních nástrojích

Když už začnete využívat C++11, tak je dobré, aby tuto novou normu podporovaly i nástroje. Jedním z hojně využívaných nástrojů pro tvorbu projektové dokumentace a popis API je program doxygen. Ten umí z komentovaných zdrojových kódů vygenerovat popis modulů, tříd, funkcí apod.

Doxygen podporuje C++11 od verze 1.8.2, vydané v srpnu tohoto roku. Zde je k nahlédnutí přehled změn.

Závěr

Doufám, že se vám můj příspěvek ukazující některé z novinek v nové normě ISO C++11 líbil. Podpora v překladačích a standardních knihovnách se každým měsícem zlepšuje a již teď můžete začíst směle využívat mnoha vlastností, které nová norma přináší.

Pokud vás něco z mého příspěvku mimořádně zaujalo, určitě to napište do komentáře. Možná se k tomu pak vrátím v některém z nadcházejících příspěvků :).

Komentáře

Mozna mi neco unika, ale nasledujici kod nejde prelozit.
Varianta s predavanim hodnotou, se ovsem uspesne prelozi.
Mozna mi neco unika ...

class Person{
    int id;
    string name;
 
  public:
    int getId() { return id; };
};
std::sort(
  people.begin(),
  people.end(),
  [](const Person& p1, const Person& p2) {   // anonymni funkce
     return p1.getId() < p2.getId();
  }
);
g++ --std=c++11 new_things.cpp -o bin_new_things -Wall -Wno-unused-variable
new_things.cpp: In lambda function:
new_things.cpp:159:23: error: passing 'const Person' as 'this' argument of 'int Person::getId()' discards qualifiers [-fpermissive]
       return p1.getId() < p2.getId();
                       ^
new_things.cpp:159:36: error: passing 'const Person' as 'this' argument of 'int Person::getId()' discards qualifiers [-fpermissive]
       return p1.getId() < p2.getId();
                                    ^
make: *** [all] Error 1

Ahoj. Ona metoda getId() musí být označena jako const, jinak ji nelze na objektu typu const Person& zavolat. Čili změň

int getId() { return id; };

na

int getId() const { return id; };

a bude to fungovat.

Přidat komentář