Nowe elementy dla autorów bibliotek

Statyczne asercje - static_assert

Nowe słowo kluczowe static_assert umożliwia stosowanie asercji, które są ewaluowane na etapie kompilacji.

Format: static_assert(condition, msg)

  • jeśli warunek condition nie jest spełniony, generuje błąd kompilacji z komunikatem msg

  • msg musi być literałem znakowym

static_assert w zasięgu klasy

Najczęściej statyczne asercje są używane w kodzie szablonowym:

template <typename T>
class OnlyCompatibleWithIntegralTypes
{
   static_assert(std::is_integral<T>::value, "T must be integral");
};

OnlyCompatibleWithIntegralTypes<double> test; // błąd kompilacji!

static_assert w zasięgu funkcji

Asercji statycznych można używać również w funkcjach lub szablonach funkcji.

template <typename T, size_t N>
void accepts_arrays_with_size_between_1_and_255(T (&arr)[N])
{
    static_assert((N >= 1) && (N <= 255), "size of array must be in range [1, 255]");
}

Uogólnione stałe wyrażenia - constexpr

C++11 wprowadza dwa znaczenia dla „stałej”:

  • constexpr - stała ewaluowana na etapie kompilacji

  • const - stała, której wartość nie może ulec zmianie

Stałe wyrażenie (constant expression) jest wyrażeniem ewaluowanym przez kompilator na etapie kompilacji. Nie może zawierać wartości, które nie są znane na etapie kompilacji i nie może mieć efektów ubocznych.

Jeśli wyrażenie inicjalizujące dla constexpr nie będzie mogło być wyliczone na etapie kompilacji kompilator zgłosi błąd:

int x1 = 7;  // variable
constexpr int x2 = 7;  // constant at compile-time

constexpr int x3 = x1; // error: initializer is not a contant expression

constexpr auto x4 = x2; // Ok

W wyrażeniu constexpr można użyć:

  • Wartości typów całkowitych, zmiennoprzecinkowych oraz wyliczeniowych

  • Operatorów nie modyfikujących stanu (np. +, ? i [] ale nie = lub ++)

  • Funkcji constexpr

  • Typów literalnych

  • Stałych const zainicjowanych stałym wyrażeniem

Stałe wartości constexpr

W C++11 constexpr przed definicją zmiennej definiuje ją jako stałą, która musi zostać zainicjowana wyrażeniem stałym.

Stała const w odróżnieniu od stałej constexpr nie musi być zainicjowana wyrażeniem stałym.

constexpr int x = 7;

constexpr auto prefix = "Data";

constexpr double pi = 3.1415;

constexpr double pi_2 = pi / 2;
(const double) 1.570750

Funkcje constexpr

W C++11 funkcje mogą zostać zadeklarowane jako constexpr jeśli spełniają dwa wymagania:

  • Ciało funkcji zawiera tylko jedną instrukcję return zwracającą wartość, która nie jest typu void

  • Typ wartości zwracanej oraz typy parametrów powinny być typami dozwolonymi dla wyrażeń constexpr

C++14 znacznie poluzowuje wymagania stawiane przed funkcjami constexpr. Funkcją constexpr może zostać dowolna funkcja o ile:

  • nie jest wirtualna

  • typ wartości zwracanej oraz typy parametrów są typami literalnymi

  • zmienne użyte wewnątrz funkcji są zmiennymi typów literalnych

  • nie zawiera instrukcji asm, goto, etykiet oraz bloków try-catch

  • zmienne użyte wewnątrz funkcji nie są statyczne oraz nie są thread-local

  • zmienne użyte wewnątrz funkcji są zainicjowane

Ważne

Funkcje constexpr nie mogą mieć żadnych efektów ubocznych. Zapisywanie stanu do nielokalnych zmiennych jest błędem kompilacji.

Przykład rekurencyjnej funkcji constexpr:

constexpr int factorial(int n)
{
    return (n == 0) ? 1 : n * factorial(n-1);
}

Funkcja constexpr może zostać użyta w kontekście, w którym wymagana jest stała ewaluowana na etapie kompilacji (np. rozmiar tablicy natywnej lub stała będąca parametrem szablonu):

#include <array>

const int size = 2;

int arr1[factorial(1)];
int arr2[factorial(size)];
std::array<int, factorial(3)> arr3;
template <typename T, size_t N>
constexpr size_t size_of_array(T(&)[N])
{
   return N;
}

int arr4[factorial(size_of_array(arr2))];
(int [2]) { 0, 0 }

Instrukcje warunkowe w funkcjach constexpr

Pominięty blok kodu w instrukcji warunkowej nie jest ewaluowany na etapie kompilacji.

constexpr int low = 0;
constexpr int high = 99;
#include <stdexcept>

constexpr int check(int i)
{
    return (low <= i && i < high) ? i : throw std::out_of_range("range error");
}
constexpr int val0 = check(50);  // Ok

constexpr int val2 = check(200);  // Error

Typy literalne

C++11 wprowadza pojęcie typu literalnego (literal type), który może być użyty w stałym wyrażeniu constexpr:

Typem literalnym jest:

  • Typ arytmetyczny (całkowity, zmiennoprzecinkowy, znakowy lub logiczny)

  • Typ referencyjny do typu literalnego (np: int&, double&)

  • Tablica typów literalnych

  • Klasa, która:

    • ma trywialny destruktor (może być default)

    • wszystkie niestatyczne składowe i typy bazowe są typami literalnymi

    • jest agregatem lub ma przynajmniej jeden konstruktor contexpr, który nie jest konstruktorem kopiującym lub przenoszącym (konstruktor musi mieć pustą implementację, ale umożliwia inicjalizację składowych na liście inicjalizującej)

class Complex
{
    double real_, imaginary_;
public:
    constexpr Complex(const double& real, const double& imaginary)
        : real_ {real}, imaginary_ {imaginary}
    {}

    constexpr double real() const { return real_; };
    constexpr double imaginary() const { return imaginary_; }
};
constexpr Complex c1 {1, 2};

Przykłady zastosowań wyrażeń i funkcji stałych (constexpr)

Operacje na polach bitowych

Interesującym zastosowaniem funkcji constexpr jest implementacja operatorów bitowych dla wyliczeń.

namespace Constexpr
{
    enum class Bitmask { b0 = 0x1, b1 = 0x2, b2 = 0x4 };

    constexpr Bitmask operator|(Bitmask left, Bitmask right)
    {
        return Bitmask( static_cast<int>(left) | static_cast<int>(right) );
    }
}

Umożliwia to, zastosowanie czytelnych wyrażeń bitowych np. w etykietach instrukcji switch:

#include <iostream>

using namespace std;
using namespace Constexpr;

Bitmask b = Bitmask::b0 | Bitmask::b1;

switch (b)
{
    case Bitmask::b0 | Bitmask::b1:
        cout << "b0 | b1 - " << static_cast<int>(b) << endl;
        break;
    default:
        cout << "Other value...";
}
b0 | b1 - 3

Variadic templates

W C++11 szablony mogą akceptować dowolną ilość (również zero) parametrów. Jest to możliwe dzięki użyciu specjalnego grupowego parametru szablonu tzw. parameter pack, który reprezentuje wiele lub zero parametrów szablonu.

Parameter pack

Parameter pack może być

  • grupą parametrów szablonu

    template<typename... Ts> // template parameter pack
    class tuple
    {
        //...
    };
    
    tuple<int, double, string&> t1; // 3 arguments: int, double, string&
    tuple<> empty_tuple; // 0 arguments
    
  • grupą argumentów funkcji szablonowej

    template <typename T, typename... Args>
    shared_ptr<T> make_shared(Args&&... params)
    {
        //...
    }
    
    auto sptr = make_shared<Gadget>(10); // Args as template param: int
                                         // Args as function param: int&&
    

Rozpakowanie paczki parametrów

Podstawową operacją wykonywaną na grupie parametrów szablonu jest rozpakowanie jej za pomocą operatora ... (tzw. pack expansion).

template <typaname... Ts>  // template  parameter pack
struct X
{
    tuple<Ts...> data;  // pack expansion
};

Rozpakowanie paczki parametrów (pack expansion) może zostać zrealizowane przy pomocy wzorca zakończonego elipsą ...:

template <typaname... Ts>  // template  parameter pack
struct XPtrs
{
    tuple<Ts const*...> ptrs;  // pack expansion
};

XPtrs<int, string, double> ptrs; // contains tuple<int const*, string const*, double const*>

Najczęstszym przypadkiem użycia wzorca przy rozpakowaniu paczki parametrów jest implementacja perfect forwarding’u:

template <typename... Args>
void make_call(Args&&... params)
{
    callable(forward<Args>(params)...);
}

Idiom Head/Tail

Praktyczne użycie variadic templates wykorzystuje często idiom Head/Tail (znany również First/Rest).

Idiom ten polega na zdefiniowaniu wersji szablonu akceptującego dwa parametry: - pierwszego Head - drugiego Tail w postaci paczki parametrów

W implementacji wykorzystany jest parametr (lub argument) typu Head, po czym rekursywnie wywołana jest implementacja dla rozpakowanej paczki parametrów typu Tail.

Dla szablonów klas idiom wykorzystuje specjalizację częściową i szczegółową (do przerwania rekurencji):

template <typename... Types>
struct Count;

template <typename Head, typename... Tail>
struct Count<Head, Tail...>
{
    constexpr static int value = 1 + Count<Tail...>::value; // expansion pack
};

template <>
struct Count<>
{
    constexpr static int value = 0;
};

//...
static_assert(Count<int, double, string&>::value == 3, "must be 3");

W przypadku szablonów funkcji rekurencja może być przerwana przez dostarczenie odpowiednio przeciążonej funkcji. Zostanie ona wywołana w odpowiednim momencie rozwijania rekurencji.

void print()
{}

template <typename T, typename... Tail>
void print(const T& arg1, const Tail&... params)
{
    cout << arg1 << endl;
    print(params...); // function parameter pack expansion
}

Operator sizeof...

Operator sizeof... umożliwia odczytanie na etapie kompilacji ilości parametrów w grupie.

template <typename... Types>
struct VerifyCount
{
    static_assert(Count<Types...>::value == sizeof...(Types),
                    "Error in counting number of parameters");
};

Forwardowanie wywołań funkcji

Variadic templates są niezwykle przydatne do forwardowania wywołań funkcji.

template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... params)
{
    return std::unique_ptr<T>(new T(forward<Args>(params)...);
}

Ograniczenia paczek parametrów

Klasa szablonowa może mieć tylko jedną paczkę parametrów i musi ona zostać umieszczona na końcu listy parametrów szablonu:

template <size_t... Indexes, typename... Ts>  // error
class Error;

Można obejść to ograniczenie w następujący sposób:

template <size_t... Indexes> struct IndexSequence {};

template <typename Indexes, typename Ts...>
class Ok;

Ok<IndexSequence<1, 2, 3>, int, char, double> ok;

Funkcje szablonowe mogą mieć więcej paczek parametrów:

template <int... Factors, typename... Ts>
void scale_and_print(const Ts&... args)
{
    print(ints * args...);
}

scale_and_print<1, 2, 3>(3.14, 2, 3.0f);  // calls print(1 * 3.14, 2 * 2, 3 * 3.0)

Uwaga! Wszystkie paczki w tym samym wyrażeniu rozpakowującym muszą mieć taki sam rozmiar.

scale_and_print<1, 2>(3.14, 2, 3.0f);  // error

„Nietypowe” paczki parametrów

  • Podobnie jak w przypadku innych parametrów szablonów, paczka parametrów nie musi być paczką typów, lecz może być paczką stałych znanych na etapie kompilacji

template <size_t... Values>
struct MaxValue;  // primary template declaration

template <size_t First, size_t... Rest>
struct MaxValue<First, Rest...>
{
    static constexpr size_t rvalue = MaxValue<Rest...>::value;
    static constexpr size_t value = (First < rvalue) ? rvalue : First;
};

template <size_t Last>
struct MaxValue<Last>
{
    static constexpr size_t value = Last;  // termination of recursive expansion
};
static_assert(MaxValue<1, 5345, 3, 453, 645, 13>::value == 5345, "Error");

Agregaty, typy POD i Standard Layout

Agregaty w C++11

Agregat (aggregate) - tablica lub klasa, która jest agregatem nie może mieć

  • Konstruktora dostarczonego przez użytkownika (user-provided constructor)

  • Niestatycznych składowych zainicjowanych w miejscu deklaracji

  • Prywatnych lub chronionych niestatycznych pól składowych

  • Klas bazowych

  • Metod wirtualnych

class NotAggregate1
{
    virtual void f(){} // no virtual functions allowed
};

class NotAggregate2
{
    int x; // x is private by default and non-static
};

class NotAggregate3
{
public:
    NotAggregate3(int) {} // user-provided constructor
};

class Aggregate1
{
public:
  NotAggregate1 member1;   // ok, public member
  Aggregate1& operator=(const Aggregate1& rhs) {/* */} // OK, copy-assignment
private:
  void f() {} // OK, just a private function
};

class Aggregate2
{
public:
    Aggregate2() = default; // OK, user-declared but not user-provided
}

Agregaty mogą być inicjalizowane nawiasami klamrowymi (nie należy mylić z inicjalizacją przy pomocy konstruktora z std::initializer_list<>). Jeśli elementów na liście jest mniej niż w deklaracji agregatu, pozostałe elementy są w miarę możliwości inicjalizowane wartościowo (tzn. typy wbudowane są zerowane a dla typów użytkownika wywoływany jest konstruktor domyślny)

struct Aggregate1
{
    int a;
    double b;
    vector<int> vec;

    Aggregate1() = default;

    void print(const string& prefix) const
    {
        cout << prefix << ": a=" << a << "; b=" << b;
        cout << "; vec=[ ";
        for(auto& item : vec)
            cout << item << " ";
        cout << "]" << endl;
    }
};

//...
Aggregate1 agg1 = {1, 4.5, {3, 4, 5, 6}};

agg1.print("agg1");

Typy POD

Idea typu POD (Plain-Old Data) została wprowadzona w C++98 aby dać wsparcie dla:

  1. Możliwości statycznej inicjalizacji typu.

  2. Kompilacji typu POD do postaci kompatybilnej bitowo (memory layout) ze strukturami języka C

Ponieważ cele te są w zasadzie rozłączne w C++11 rozdzielono je wprowadzając dwa różne koncepty:

  • Klasy trywialne - tirivial classes

  • Klasy standard-layout

Ważne

Nowa definicja typu POD w C++11 mówi, że klasa POD to klasa, która jest zarówno trywialna (trivial) jak i ma standardowy layout. Właściwość ta musi być rekursywnie spełniona dla wszystkich niestatycznych pól składowych.

Klasy trywialne

Klasy trywialne (trivial classes) wspierają statyczną inicjalizację. Jeśli klasa jest trywialnie kopiowalna może być użyta do kopiowania bitowego np. memcpy().

Standard definiuje klasę trywialnie kopiowalną (trivialy copyable) jako klasę, która:

  • nie ma nietrywialnego konstruktora kopiującego

  • nie ma nietrywialnego konstruktora przenoszącego

  • nie ma nietrywialnego kopiującego operatora przypisania

  • nie ma nietrywialnego przenoszącego operatora przypisania

  • ma trywialny destruktor

Klasa trywialna to klasa, która ma trywialny konstruktor domyślny i jest trywialnie kopiowalna. W szczególności klasa trywialna nie ma wirtualnych metod oraz wirtualnych klas bazowych.

Kopiujący/Przenoszący konstruktor jest trywialny, jeśli nie jest zdefiniowany przez użytkownika user-provided oraz dodatkowo:

  • klasa nie posiada wirtualnego konstruktora oraz wirtualnych klas bazowych

  • konstruktor kopiujący/przenoszący klasy bazowej jest trywialny

  • każde niestatyczne pole składowe ma trywialny konstruktor kopiujący/przenoszący

Podobne wymagania są zdefiniowane dla trywialnego kopiującego/przenoszącego operatora przypisania.

// empty classes are trivial
struct Trivial1 {};

// all special members are implicit
struct Trivial2
{
    int x;
};

struct Trivial3 : Trivial2 // base class is trivial
{
    Trivial3() = default; // not a user-provided ctor
    int y;
};

struct Trivial4
{
public:
    int a;
private: // no restrictions on access modifiers
    int b;
};

struct Trivial5
{
    Trivial1 a;
    Trivial2 b;
    Trivial3 c;
    Trivial4 d;
};

struct Trivial6
{
    Trivial2 a[23];
};

struct Trivial7
{
    Trivial6 c;
    void f(); // it's okay to have non-virtual functions
};

struct Trivial8
{
     int x;
     static NonTrivial1 y; // no restrictions on static members
}

struct Trivial9
{
     Trivial9() = default; // not user-provided

     // a regular constructor is okay because we still have default ctor
     Trivial9(int x) : x(x) {};
     int x;
}

struct NonTrivial1 : Trivial 3
{
    virtual f(); // virtual members make non-trivial ctors
}

struct NonTrivial2
{
    NonTrivial2() : z(42) {} // user-provided ctor
    int z;
}

struct NonTrivial3
{
    NonTrivial3(); // user-provided ctor
    int w;
}

NonTrivial3::NonTrivial3() = default; // defaulted but not on first declaration
                                      // still counts as user-provided
struct NonTrivial5
{
    virtual ~NonTrivial5(); // virtual destructors are not trivial
};

Typy standard-layout

Typy standard-layout mają takie same ułożenie pól w pamięci jak struktury w języku C.

Klasy standard-layout:

  • nie mają metod wirtualnych i wirtualnych klas bazowych

  • wszystkie niestatyczne pola składowe są typu standard-layout lub referencją do takich typów

  • wszystkie niestatyczne pola składowe mają taki sam kwalifikator dostępu (public, protected lub private)

  • klasa bazowa jest typu standard-layout

  • gdy używamy dziedziczenia, tylko jedna klasa w całej hierarchii dziedziczenia może mieć niestatyczne pola składowe i pierwsze niestatyczne pole składowe nie może być typu klasy bazowej.

// empty classes have standard-layout
struct StandardLayout1 {};

struct StandardLayout2
{
    int x;
};

struct StandardLayout3
{
private: // both are private, so it's ok
    int x;
    int y;
};

struct StandardLayout4 : StandardLayout1
{
    int x;
    int y;

    void f(); // perfectly fine to have non-virtual functions
};

struct StandardLayout5 : StandardLayout1
{
    int x;

    StandardLayout1 y; // can have members of base type if they're not the first
};

struct StandardLayout6 : StandardLayout1, StandardLayout5
{
    // can use multiple inheritance as long only
    // one class in the hierarchy has non-static data members
};

struct StandardLayout7
{
    int x;
    int y;

    StandardLayout7(int x, int y) : x(x), y(y) {} // user-provided ctors are ok
};

struct StandardLayout8
{
public:
    StandardLayout8(int x) : x(x) {} // user-provided ctors are ok
    // ok to have non-static data members and other members with different access
private:
    int x;
};

struct StandardLayout9
{
    int x;

    static NonStandardLayout1 y; // no restrictions on static members
};

struct NonStandardLayout1
{
    virtual f(); // cannot have virtual functions
};

struct NonStandardLayout2
{
    NonStandardLayout1 X; // has non-standard-layout member
};

struct NonStandardLayout3 : StandardLayout1
{
    StandardLayout1 x; // first member cannot be of the same type as base
};

struct NonStandardLayout4 : StandardLayout3
{
    int z; // more than one class has non-static data members
};

struct NonStandardLayout5 : NonStandardLayout3 {}; // has a non-standard-layout

Wersjonowanie bibliotek - inline namespace

Przestrzenie nazw inline ułatwiają wersjonowanie bibliotek i wprowadzanie ich nowych wersji bez potrzeby zmiany kodu po stronie klienta.

Wszystkie symbole umieszczone w przestrzeni nazw inline są automatycznie dostępne w przestrzeni nazw, która zawiera przestrzeń inline.

namespace Library
{
    inline namespace ver_3_2
    {
        double foo();

        void do_something(int);

        template <typename T>
        class Gadget
        {
            // implementation
        };
    }

    namespace ver_3_0
    {
        void do_something(int);

        template <typename T>
        class Gadget
        {
            // implementation
        };
    }

    namespace ver_2_9
    {
        template <typename T>
        class Gadget
        {
            // implementation
        };
    }
}

Użycie biblioteki Library wygląda następująco:

using namespace Library;

do_something(6);

auto result1 = foo();
auto result2 = ver_3_0::foo();
auto result3 = ver_2_9::foo();

template <class T>
Library::Gadget<T*>
{
    //...
};

Duplikacja kodu może być uniknięta, jeśli zastosujemy odpowiednio dyrektywy #include.

// file v_3_common.hpp

// declarations
int global;

Plik v_3_common.hpp zawiera wspólny kod dla różnych wersji bibliotek i może być wielokrotnie włączony przy pomocy #include.

// file v_3_2.hpp

namespace v_3_2
{
    double foo();

    void do_something(int);

    template <typename T>
    class Gadget
    {
        // implementation
    };

    #include "v_3_common.hpp";
}
// file v_3_0,hpp

namespace v_3_0
{
    #include "v_3_common.hpp";
}

Plik nagłówkowy biblioteki Library:

namespace Library
{
    inline
    #include "v_3_2.hpp";
    #include "v_3_0,hpp";
    #include "v_2_9.hpp";
}