Jste zde

Méně známé novinky v C++11 a C++14

V příspěvku bych chtěl popsat některé novinky v C++11 a C++14, ke kterým jsem se nedostal ve svých předchozích příspěvcích Co je nového v C++11 a Co je nového v C++14.

C++11

Začněme C++11. Pak přejdeme na C++14.

Referenční kvalifikátory (reference qualifiers)

Je všeobecně známé, že pomocí const kvalifikátoru jste schopni rozlišit, zda je metoda volána na konstantním či nekonstantním objektu. Příklad z std::vector:

iterator begin();             // (1)
const_iterator begin() const; // (2)

Pokud tedy zavoláte begin() na konstantním vektoru, zavolá se metoda (2), jinak se zavolá metoda (1). Nic nového.

C++11 spolu s r-hodnotovými referencemi přináší i možnost rozlišení, zda je metoda volána na l-hodnotě či r-hodnotě pomocí tzv. referenčních kvalifikátorů & a &&:

class A {
public:
    void foo()  & { std::cout << "foo() &"; }
    void foo() && { std::cout << "foo() &&"; }
};
 
A a;
a.foo();   // Vypíše "foo() &", protože 'a' je l-hodnota.
A().foo(); // Vypíše "foo() &&", protože 'A()' je r-hodnota.

Odkazy: 0, 1, 2, 3

Inline prostory jmen (inline namespaces)

C++ zavádí tzv. inline prostory jmen, jež automaticky zpřístupní všechny své členy v obalujícím (vnějším) prostoru jmen.

namespace A {
    inline namespace B {
        class C {};
    }
}
 
A::B::C c1; // OK (očekávaně).
A::C c2;    // Taktéž OK, protože B je inline.

Tento mechanismus umožňuje např. verzování knihoven na úrovni jazyka.

Odkazy: 0, 1, 2, 3

Externí šablony (extern templates)

Pokud překladač v C++98 narazí na plně specifikovanou šablonu (např. std::vector<int> v;), musí provést její instanciaci do aktuálně překládaného modulu (angl. translation unit). To zbytečně prodlužuje překlad v situacích, kdy je stejná šablona se stejnými typy použita ve více modulech. Dále to komplikuje linkování, protože linker by měl duplicitní instanciace odstranit, aby výsledný program zbytečně neobsahoval duplicitní kód.

C++11 umožňuje s využitím klíčového slova extern říct, že daná šablona bude instanciována jinde (v jiném modulu):

extern template class std::vector<int>;

V některém z překládaných modulů pak musí být explicitní instanciace dané šablony:

template class std::vector<int>;

Připomíná to externí proměnné z C či C++ (význam je podobný, včetně stejného klíčového slova).

Odkazy: 0, 1, 2

Atributy (standardized attribute syntax)

C++11 zavádí standardní zápis pro atributy. Dříve si každý překladač řešil specifikaci atributů po svém (např. __attribute__ u GCC). Standardizovaný zápis má tvar [[atribut(parametry)]] (parametry jsou volitelné). Zároveň zavádí dva standardní atributy:

  • [[noreturn]]: Indikuje, že daná funkce nevrací řízení zpět volajícímu. O takových funkcích jsem zde už psal. Příklad:
    [[noreturn]] void my_exit(int exit_code) {
        std::cerr << "exiting with " << exit_code << "\n";
        std::exit(exit_code);
    }
  • [[carries_dependency]]: Umožňuje překladači lépe optimalizovat vícevláknový kód (vysvětlení).

C++14 pak zavádí další standardní atribut: [[deprecated]], o němž jsem již psal.

Odkazy: 0, 1, 2

Dědění konstruktorů (inheriting constructors)

Mějme bázovou třídu A:

class A {
public:
    A(int i, int j): i(i), j(j) {}
 
private:
    int i;
    int j;
};

Pokud jste v C++98 dědili třídu B z třídy A a chtěli jste u B použít stejné konstruktory, jako má A, museli jste si delegaci napsat sami:

// C++98
class B: public A {
    B(int i, int j): A(i, j) {}
};
 
// Bez definování konstruktoru v B výše by se následující kód nepřeložil.
B b(1, 2);

C++11 umožňuje "dědění" konstruktorů pomocí using:

// C++11
class B: public A {
    using A::A;
};
 
B b(1, 2); // OK.

Zjednoduší to kód.

Odkazy: 0

Delegování konstruktorů (delegating constructors)

V C++98 nelze volat z konstruktoru jiný konstruktor téže třídy. C++11 to však umožňuje:

class A {
public:
    A(): A(1) {} // Delegace.
    A(int i): i(i) {}
 
private:
    int i;
};

Odkazy: 0, 1

Nový specifikátor 'noexcept'

Pomocí specifikátoru noexcept lze specifikovat, zda daná funkce nevyhazuje výjimky. Např. následující deklarace říká, že foo() nikdy nevyhodí výjimku:

void foo() noexcept;

Ve standardní knihovně je takto označená např. metoda std::vector::size():

size_type size() const noexcept;

Tato informace je jednak užitečná pro programátory a jednak pro překladače, protože jim umožňuje generovat efektivnější kód.

Poznámky:

  • V C++98 se k podobnému účelu používalo throw(), patřící do dynamické specifikace výjimek. Tato dynamická specifikace je ale od C++11 zavržená (angl. deprecated).
  • Překladač nijak nekontroluje, zda k vyvolání výjimky skutečně nemůže dojít. To je na programátorovi. Pokud by k vyvolání došlo, tak dojde k zavolání std::terminate() [C++11, 15.5.1], což typicky ukončí program zavoláním std::abort().
  • Pomocí noexcept(expr) lze deklarovat, že daná funkce nevyhazuje výjimku pouze za určitých předpokladů, které jsou dány výrazem expr.

Odkazy: 1, 2, 3

Nové operátory 'alignof' a 'alignas' (zarovnání)

Pomocí nových operátorů alignof a alignas se lze dotazovat na zarovnání typů v paměti a naopak zarovnání specifikovat.

// Implicitní zarovnání ().
struct A {
    char c;
    int i;
};
 
// Explicitně vyžádané zarovnání na 16 bajtů.
struct alignas(16) B {
    char c;
    int i;
};
 
std::cout << alignof(A) << "\n"; // Vytiskne implementačně závislou hodnotu (např. 4).
std::cout << alignof(B) << "\n"; // Vytiskne 16.

Pro jistotu: Céčkový operátor sizeof vrací velikost daného typu v paměti, nikoliv zarovnání.

Odkazy: 0, 1, 2, 3

Explicitní konverzní operátory (explicit conversion operators)

C++98 zavedlo specifikátor explicit, pomocí kterého lze omezit použití konstruktorů beroucí jeden argument k implicitním konverzím. Bohužel, vyřešila se tím jen část problému. Pokud totiž třída definovala konverzní operátory (např. operator bool()), tak libovolná instance dané třídy mohla být potichu implicitně konvertována na typ, který nebyl zamýšlen (např. z objektu na bool a pak na libovolný celočíselný typ).

V C++98 se to muselo řešit oklikou, pomocí tzv. safe bool idiomu. Od C++11 však lze označit jako explicitní i konverzní operátory:

class A {
public:
    explicit operator bool() const {
        return false;
    }
};
 
void func(int i);
 
A a;
if (a) { /* ... */ } // OK, jedná se o bool kontext.
func(a);             // Chyba při překladu (nelze implicitně konvertovat a na int).

Pokud bychom onen konverzní operátor neoznačili jako explicit, tak by volání func(a) výše prošlo přes posloupnost konverzí a -> bool -> int.

Odkazy: 0, 1, 2

"Raw" řetězcové literály (raw string literals)

Při vytváření regulárních výrazů či vkládání kusů kódu se hodí mít možnost vytvořit řetězcový literál, kde nemusíte escapovat lomítka a uvozovky a můžete jednoduše vytvořit literál přes více řádků. To je od C++11 umožněno pomocí tzv. "raw" řetězcových literálů:

// Poznámka: Chybného obarvení syntaxe si nevšímejte.
 
std::regex pattern(R"(\d{1,3}:"[a-d]")"); // Matchuje např. 27:"c".
 
std::string code(R"(
    int main() {
        return 0;
    }
)");

Funguje to tak, že všechno mezi R"( a )" se bere jako součást řetězce. Obecný tvar je R"xxx(...)xxx", kde xxx je vámi zvolený oddělovač (klidně prázdný). To však jen pokud byste někde v řetězci potřebovali mít sekvenci )".

Odkazy: 0, 1, 2

Unicode řetězcové literály (Unicode string literals)

V C++98 se pro práci s řetězci obsahující vícebajtové znaky dal použít typ wchar_t. Jeho zásadním nedostatkem však bylo, že velikost a sémantika byla implementačně závislá. Pro přenositelnou reprezentaci Unicode (např. UTF-16) se tedy nehodil. C++11 to řeší zavedením nových typů char16_t a char32_t a nových standardních literálů:

const char     *s1 = u8"UTF-8 řetězcový literál.";
const char16_t *s2 = u"UTF-16 řetězcový literál.";
const char32_t *s3 = U"UTF-32 řetězcový literál.";

Další související změnou je, že v řetězcích a znacích je nyní možné zapisovat Unicode kódové body (angl. code points) přes \uNNNN a \UNNNNNNNN.

Odkazy: 0a, 0b, 1, 2

Uživatelsky definované literály (user-defined literals)

V jazycích C a C++ existuje řada standardních literálů, např.

0    // Nula typu 'int'
2L   // Dvojka typu 'long int'
3.0f // Trojka typu 'float'

C++11 zavádí možnost vytváření uživatelských literálů pomocí vlastních suffixů. Např. literál typu std::string lze definovat následovně:

std::string operator "" _s(const char *str, std::size_t length) {
    return std::string(str, length);
}

Použití vypadá takto:

std::string s1 = "abc\x00xyz";   // V s1 bude "abc".
std::string s2 = "abc\x00xyz"_s; // V s2 bude "abc\x00xyz".

Všimněte si, že v prvním případě obsahuje vytvořený std::string pouze tři znaky, protože onen literál je typu const char *, u kterého nulový bajt značí jeho konec.

Předešlu, že v C++14 je tento literál přímo ve standardu, jen má jméno s místo _s (suffixy nezačínající podtržítkem jsou vyhrazené standardem, proto jsme místo s museli použít _s).

Odkazy: 0, 1

Neomezené unie (unrestricted unions)

C++11 snižuje omezení na typ objektů, které se mohou vyskytovat uvnitř unií. Např. je nyní možné, aby se uvnitř unie nacházel objekt s netriviálním konstruktorem:

struct Range {
    Range(int start, int end): start(start), end(end) {}
    int start;
    int end;
};
 
union U {
    int i;
    Range r; // Zakázáno v C++98, OK v C++11.
 
    // Kvůli Range (má netriviální konstruktor) je potřeba definovat
    // konstruktor pro U.
    U(): r(0, 0) {}
};

Odkazy: 0, 1, 2

Rozšířené friend deklarace (extended friend declarations)

C++11 zavádí tzv. rozšířenou deklaraci přátelských tříd, kdy před názvem třídy již není potřeba používat klíčové slovo class:

class A;
class B {
    friend class A; // Klasická friend deklarace (OK jak v C++98, tak v C++11).
    friend A;       // Rozšířená friend deklarace (OK od C++11).
};

Výhoda je, že od C++11 lze pomocí této syntaxe deklarovat šablonové typy jako přátele:

template <typename T>
class C {
    friend T; // Rozšířená friend deklarace (OK od C++11).
};

Toto pomocí klasické deklarace (C++98) nelze zapsat:

template <typename T>
class C {
    friend class T; // error: using template type parameter ‘T’ after ‘class’
};

Skutečně, C++98 něco takového neumožňuje. Rozšířená friend deklarace byla zavedena právě z tohoto důvodu.

Odkazy: 0, 1

Dopředné deklarace výčtů (forward declaration of enumerators)

Výčty lze od C++11 dopředně deklarovat (angl. forward declaration). Je však u nich potřeba uvést bázový typ, což je taktéž novinka od C++11. Vypadá to takto:

enum A : short;       // OK, bázový typ je short.
enum B;               // Chyba při překladu (chybí bázový typ).
enum class C : short; // OK (tzv. silně typovaný výčet), bázový typ je short.
enum class D;         // OK (tzv. silně typovaný výčet), bázový typ je implicitně int.

Toto v C++98 nebylo možné, právě z důvodu, že bez seznamu prvků výčtu nebylo možno vědět, jak bude onen výčet velký (a tudíž jaký datový typ k jeho reprezentaci použít). O silně typovaných výčtech jsem psal v původním příspěvku.

Odkazy: 0

Umožnění použití sizeof na datových složkách bez nutnosti existence objektu

Mějme následující strukturu:

struct A {
    int i;
};

Pokud byste chtěli zjistit velikost A::i v C++98, museli byste použít objekt:

// C++98
sizeof(A::i) // Chyba při překladu:
             // error: invalid use of non-static data member 'i'
 
A a;
sizeof(a.i)  // OK.

Od C++11 není objekt potřeba:

sizeof(A::i) // OK (C++11).

Odkazy: 0, 1

Lokální a bezejmenné typy jako parametry šablon (local and unnamed types as template arguments)

C++11 umožňuje jako parametry šablon použít i lokální či bezejmenné typy:

template <class T>
void func(T t) {}
 
// Bezejmenný typ.
enum { e };
 
int main() {
    // Lokální typ.
    struct A { int i; };
    A a;
 
    func(e); // OK (v C++98 chyba).
    func(a); // OK (v C++98 chyba).
}

Odkazy: 0

Vylepšení kompatibility s C99

Z důvodu zlepšení kompatibility s C99 přibylo do C++11 následující:

  • Typ long long int. Alespoň 64b celočíselný typ (samozřejmě existuje i jeho unsigned verze). Před tím byl v C++ akorát long int, jehož velikost byla garantována pouze 32b (mohla být samozřejmě větší, ale to nebylo z hlediska standardu zaručeno).
  • Identifikátor __func__. Název funkce, ve které je tento identifikátor použit. Hodí se to např. u ladicích výpisů.
  • Podpora pro makra s proměnným počtem parametrů (angl. variadic macros).
  • Hlavičkové soubory. Přibyly hlavičkové soubory <ccomplex>, <cstdbool>, <cstdint>, <cinttypes> a <ctgmath>.
  • A mnoho dalších drobností...

Některé překladače však výše uvedené již dříve poskytovaly jako rozšíření.

Odkazy: 0a, 0b, 0c, 1

Novinky ve standardní knihovně

Zde bohužel jen krátce, protože příspěvek už je i tak dost dlouhý. Snad se k detailnějšímu rozboru novinek standardní knihovny dostanu v budoucnu. Určitě by si to zasloužila.

Některé méně známé nové hlavičkové soubory:

  • <array>: Pole o fixní velikosti (aneb Céčková pole v kontejnerovém kabátu).
  • <atomic>: Atomické operace.
  • <cfenv>: Zjišťování informací o typech s plovoucí řadovou čárkou a nastavování chování (např. zaokrouhlování).
  • <chrono>: Kolekce typů pro reprezentaci času.
  • <forward_list>: Jednosměrně vázaný seznam (std::list je obousměrně vázaný).
  • <initializer_list>: Proxy objekt poskytující přístup k poli objektů (automaticky konstruován ze seznamu prvků ve složených závorkách, např. u std::vector<int> v = {1, 2, 3};).
  • <random>: Rozšiřitelné generátory a rozložení náhodných čísel. Již nikdy víc standardní Céčková funkce rand() (motivace).
  • <ratio>: Přesná reprezentace racionálních čísel.
  • <thread>, <mutex>, <future>, <condition_variable>: Standardní knihovna pro práci s vlákny.
  • <type_traits>: Tzv. typové rysy pro zjišťování informací o typech a jejich modifikace (např. odstranění reference).
  • <typeindex>: Wrapper nad std::type_info, který je možno používat v asociativních kontejnerech.

Některé méně známé novinky v existujících hlavičkových souborech (opravdu jen velmi malá část, novinek je hodně):

C++14

Uff, u C++11 toho bylo hodně, že? Vrhněme se nyní na C++14, kde je těch novinek, které jsem dříve nezmiňoval o poznání méně.

Agregáty a inicializátory členských složek (member initializers and aggregates)

C++14 snížilo omezení z C++11 kladená na tzv. agregáty. Konkrétně je nyní umožněno, aby agregáty obsahovaly inicializátory členských složek:

struct A {
    int i = 0;
    double j = 0.0;
};
 
A a = {1};      // OK (C++14), chyba při překladu v C++11.
A b = {1, 2.0}; // OK (C++14), chyba při překladu v C++11.

Ona inicializace přes složené závorky použitá výše je klasická Céčková inicializace struktur. Pokud by vás zajímaly detaily, mrkněte na můj příspěvek na toto téma, kde je vše detailně vysvětleno.

Odkazy: 0, 1, 2

constexpr metody nejsou implicitně const

Nadpis mluví za vše: od C++14 nejsou constexpr metody implicitně const:

class Point {
public:
    constexpr Point(int x, int y): x(x), y(y) {}
 
    // V C++11 by následující metody byly implicitně const.
    // constexpr int getX() { return x; }
    // constexpr int getY() { return y; }
 
    // Od C++14 tomu tak není, takže pokud chceme mít const constexpr
    // metody, musíme to napsat explicitně.
    constexpr int getX() const { return x; }
    constexpr int getY() const { return y; }
 
private:
    int x;
    int y;
};

Odkazy: 0a, 0b

Zjednodušení zápisu transformačních typových rysů (TransformationTraits redux)

Místo některých typových rysů tvaru xxx<T>::type lze nyní psát jen xxx_t<T>. Příklad:

// C++11
using TWithoutRef = typename std::remove_reference<T>::type;
 
// C++14
using TWithoutRef = std::remove_reference_t<T>;

Zpřehlední to kód.

Odkazy: 0

Heterogenní vyhledávání v asociativních kontejnerech (heterogeneous comparison lookup in associative containers)

C++14 umožňuje, aby se v kontejnerech jako je std::set či std::map dalo vyhledávat i podle objektů jiného typu, než je typ klíčů v daném kontejneru:

#include <functional>
#include <set>
 
struct A {
    explicit A(int i): i(i) {}
 
    int i;
};
 
inline bool operator<(const A &a, const int &i) { return a.i < i; }
inline bool operator<(const int &i, const A &a) { return i < a.i; }
 
int main() {
    std::set<A, std::less<>> s; // std::less<> je z C++14.
 
    // ...
 
    // OK, i přesto, že 520 nelze implicitně konvertovat na A.
    auto it = s.find(520);
    if (it != s.end()) {
        // ...
    }
}

Odkazy: 0a, 0b, 1, 2, 3

Dealokace paměti s určením velikosti (sized deallocation)

C++14 zavádí možnost si nadefinovat vlastní operator delete, který kromě ukazatele přebírá i velikost:

int *p = new int;
::operator delete(p, sizeof(int));

Samozřejmě, příklad výše je pouze ilustrativní z hlediska syntaxe.

Odkazy: 0

Zdrojové kódy

Zdrojové kódy ke všem příkladům jsou k dispozici u mě na GitHubu.

Komentáře

Bych asi řekl, že kometář k raw string literals by měl být spíše takto:
Funguje to tak, že všechno mezi R"( a )" se bere jako součást řetězce. Obecný tvar je R"xxx(...)xxx", kde xxx je vámi zvolený oddělovač (klidně prázdný). To však jen pokud byste někde v řetězci potřebovali mít sekvenci )".

Ano, byl to překlep. Opraveno. Díky.

Přidat komentář