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 komunikatemmsg
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 kompilacjiconst
- 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 typuvoid
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ówtry-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:
Możliwości statycznej inicjalizacji typu.
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
lubprivate
)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";
}