Biblioteka standardowa¶
std::array<T, N>¶
std::array<T, N>
implementuje tablicę o stałym rozmiarze (fixed size array).
Spełnia (prawie) wszystkie wymagania dla kontenera przez co jest dużo wygodniejsza w użyciu niż tablica typu C-style.
Plik nagłówkowy <array>
namespace std
{
template <typename T, size_t Size>
struct array
{
T _Elems[Size];
using value_type = T;
using size_type = size_t;
using difference_type = ptrdiff_t;
using pointer = T *;
using const_pointer = const T *;
using reference = T&;
using const_reference = const T&;
using iterator = _Array_iterator<_Ty, _Size>;
using const_iterator = _Array_const_iterator<_Ty, _Size>;
using reverse_iterator = _STD reverse_iterator<iterator>;
using const_reverse_iterator = _STD reverse_iterator<const_iterator>;
constexpr reference operator[](size_type pos) noexcept
{
return _Elems[pos];
}
constexpr iterator begin() noexcept
{ // return iterator for beginning of mutable sequence
return (iterator(_Elems, 0));
}
constexpr iterator end() noexcept
{ // return iterator for end of mutable sequence
return (iterator(_Elems, _Size));
}
//... rest of implementation
};
}
Inicjalizacja¶
Ponieważ std::array
jest agregatem, możliwa jest inicjalizacja agregatowa.
Jest też jedynym kontenerem standardowym, którego elementy są inicjalizowane w sposób domyślny (default initialized).
std::array<int, 4> a; // items of a have undefined value
std::array<int, 4> arr1 = { {} }; // { 0, 0, 0, 0 }
std::array<int, 4> arr2 = { { 1, 2 } }; // { 1, 2, 0, 0 }
std::array<int, 4> arr3 = { { 1, 2, 3, 4 } }; // { 1, 2, 3, 4 };
std::array<int, 6> arr4 = { 1, 2, 3, 4, 5, 6 }; // possible warning
Od C++17 można w celu inicjalizacji wykorzystać mechanizm dedukcji parametrów klasy szablonowej:
std::array arr = { 1, 2, 3, 4, 5, 6 }; // std::array<int, 6>
static_assert(arr.size() == 6);
static_assert(is_same_v<decltype(arr)::value_type, int>);
for(const auto& item : arr)
std::cout << item << ' ';
std::cout << '\n';
Zwracanie tablicy z funkcji¶
Jedną z zalet tablic std::array
jest to, że można je zwracać z funkcji:
auto cross_product(const std::array<int, 3>& a, const std::array<int, 3>& b) -> std::array<int, 3>
{
return {{
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
}};
}
Interoperacyjność z tablicami C¶
Metoda data()
zwraca wskaźnik do typu danych przechowywanych w tablicy. Umożliwia to
współpracę z kodem używającym klasycznych tablic C.
namespace LegacyCode
{
void use_tab(int* tab, size_t size)
{
std::cout << "using tab: ";
for (int* it = tab; it != tab + size; ++it)
std::cout << *it << " ";
std::cout << '\n';
}
}
std::array arr1 = { 1, 2, 3, 4, 5 };
LegacyCode::use_tab(arr.data(), arr.size());
Można wykorzystać std::array<char>
jako bufor dla C-string’ów:
std::array<char,255> cstr; // create static array of 41 chars
strcpy(cstr.data(),"hello, world"); // copy a C-string into the array
printf("%s\n", cstr.data()); // print contents of the array as C-string
Zamiana danych - swap()¶
Metoda swap()
umożliwia wymianę danych w tablicach tego samego typu. Złożoność tej operacji jest liniowa.
std::array<int, 4> arr1 = { { 1, 2, -1, 4} };
std::array<int, 4> arr2 = { {} };
utils::print(arr1, "arr1: ");
utils::print(arr2, "arr2: ");
arr1.swap(arr2);
cout << "\nAfter swap:\n";
utils::print(arr1, "arr1: ");
utils::print(arr2, "arr2: ");
arr1: [ 1 2 -1 4 ]
arr2: [ 0 0 0 0 ]
Swap:
arr1: [ 0 0 0 0 ]
arr2: [ 1 2 -1 4 ]
Interfejs krotki¶
Dla typu std::array
zaimplementowany jest interfejs krotki (tuple interface). W rezultacie
można traktować obiekty tablic jako homogeniczne krotki.
using ArrayAsTuple = std::array<int, 4>;
ArrayAsTuple t = { { 1, 2, 3, 4 } };
static_assert(std::tuple_size<ArrayAsTuple>::value == 4);
static_assert(std::is_same_v<std::tuple_element<1, ArrayAsTuple>::type, int>);
assert(std::get<1>(t) == 2);
Kontenery haszujące¶
Kontenery haszujące gwarantują średnio stałą złożoność czasową operacji wyszukiwania, wstawiania oraz usuwania. Elementy w kontenerze haszującym nie są posortowane, ale są przechowywane w tzw. kubełkach (buckets). Element trafia do określonego kubełka na podstawie wartości skrótu (hash value) obliczanej dla elementu.
C++11 wprowadza następujące kontenery haszujące
- zbiory:
unordered_set
iunordered_multiset
- mapy:
unordered_map
iunordered_multimap
std::unordered_set<T>¶
- Plik nagłówkowy
<unordered_set>
namespace std
{
template<
typename Key,
typename Hash = std::hash<Key>,
typename Pred = std::equal_to<Key>,
typename Alloc = std::allocator<Key>
> class unordered_set
{
//...
};
}
- Zbiór haszujący akceptujący duplikaty kluczy:
unordered_multiset
- Kontener
unordered_set
wymaga podania jako parametrów szablonu:- Funktora lub funkcji haszującej klucze - domyślnie
std::hash<T>
- Predykatu równości elementów - domyślnie
std::equal_to<T>
- Funktora lub funkcji haszującej klucze - domyślnie
Ważne
Dla kluczy typu Key
, które są sobie równe k1 == k2
, musi być spełniona równość std::hash<Key>(k1) == std::hash<Key>(k2)
.
Metody dostępowe dla kubełków¶
Kontenery z haszowaniem dostarczają zbiór metod, które umożliwiają zarządzanie strukturą kubełków w kontenerze.
-
size_type
bucket_count
() const Ilość kubełków
-
size_type
max_bucket_count
() const Górna granica ilości kubełków
-
size_type
bucket_size
(size_type n) Rozmiar kubełka numer
n
-
size_type
bucket
(const key_type &k) Indeks kubełka zawierającego klucz
k
-
float
load_factor
() const Średnia ilość elementów w kubełku
-
float
max_load_factor
() const Bieżąca maksymalna ilość elementów w kubełku
-
float
max_load_factor
(float ml) Zmiana poziomu zapełnienia kubełków (ml jest traktowane jako podpowiedź)
-
void
rehash
(size_type n) Zmienia ilość kubełków na co najmniej n
Przykład
#include <random>
#include <unordered_set>
std::random_device rd;
std::default_random_engine rnd_engine{rd};
std::uniform_int_distribution<> rnd_distr{rnd_engine};
std::unordered_set<int> uset1;
for(size_t i = 0; i < 1000; ++i)
uset1.insert(rnd_distr());
cout << "\nIlosc wystapien elementu o wartosci 50: "
<< uset1.count(50) << endl;
if (auto pos = uset1.find(50); pos != uset1.end())
cout << "Znaleziony element: " << *pos << endl;
std::unordered_map<K, T>¶
- Plik nagłówkowy:
<unordered_map>
namespace std
{
template
<
typename Key, class T,
typename Hash = std::hash<Key>,
typename Pred = std::equal_to<Key>,
typename Alloc = std::allocator<std::pair<const Key, T> >
> class unordered_map
{
//...
};
}
- Jako parametry szablonu klasy przekazywane są typy:
Key
- kluczT
- mapowany typHash
- funktor wyliczający skrót dla kluczaPred
- predykat porównujący klucze
- Mapa haszująca dopuszczająca duplikaty kluczy:
unordered_multimap
Przykład:
std::unordered_map<int, string> umap1 = { { 7, "nd"}, {6, "sob"} };
umap1.insert(pair(1, "pon"));
umap1.insert(make_pair(2, "wt"));
umap1.insert(std::unordered_map<int, string>::value_type(3, "sr"));
umap1[5] = "pt";
if ( auto where = umap1.find(3); where != umap1.end())
std::cout << "Znaleziono wpis: " << where->first << " - "
<< where->second << std::endl;
std::cout << "6: " << umap1[6] << std::endl;
std::hash<T>¶
Struktura szablonowa std::hash
definiuje obiekt funkcyjny, który umożliwia wyliczenie wartości skrótu.
Operator wywołania funkcji spełnia poniższe wymagania:
- Akceptuje argument typu
Key
- Zwraca wartość typu
size_t
- Nie rzuca wyjątków
- Dla dwóch wartości
k1
ik2
, które są sobie równestd::hash<Key>()(k1) == std::hash<Key>()(k2)
- Dla dwóch wartości, które nie są sobie równe, prawdopodobieństwo, że wyliczona dla nich zostanie taka sama wartość skrótu powinna być bardzo mała.
Standard specjalizuje szablon std::hash
dla:
- wszystkich typów liczbowych
- typów wskaźnikowych:
T*
,std::unique_ptr
orazstd::shared_ptr
- typów łańcuchowych
std::bitset
std::vector<bool>
std::thread::id
std::type_index
Tworzenie funkcji haszujących¶
Jeżeli typem klucza kontenera z haszowaniem jest typ tworzony przez użytkownika, to należy dostarczyć jako parametr typu kontenera specjalizowaną funkcję haszującą. Może ty zostać zrealizowane na dwa sposoby:
Klasa funktora.
struct Person { std::string first_name; std::string last_name; bool operator==(const Person& other) const { return std::tie(first_name, last_name) == std::tie(other.first_name, other.last_name); } }; template <typename T> struct HashPerson { size_t operator()(const Person& p) const { std::size_t h1 = std::hash<std::string>()(p.first_name); std::size_t h2 = std::hash<std::string>()(p.last_name); return h1 ^ (h2 << 1); // or use boost::hash_combine } }; std::unordered_set<Person, HashPerson> people;
Specjalizacja klasy
std::hash
.namespace std { template<> struct hash<Person> { typedef Person argument_type; typedef std::size_t result_type; result_type operator()(argument_type const& p) const { result_type const h1 ( std::hash<std::string>()(p.first_name) ); result_type const h2 ( std::hash<std::string>()(p.last_name) ); return h1 ^ (h2 << 1); // or use boost::hash_combine } }; } std::unordered_set<Person> people;
Krotki - std::tuple<T…>¶
Krotki – motywacja¶
Chcemy napisać funkcję zwracającą trzy wartości statystyk dla wektora liczb całkowitych:
- wartość minimalną
- wartość maksymalną
- wartość średnią
Problem – funkcja może zwrócić tylko jedną wartość
Rozwiązanie 1 – przekazanie parametrów przez referencję
void calculate_stats(const vector<int>& data, int& min, int& max, double& avg)
{
min = *min_element(data.begin(), data.end());
max = *max_element(data.begin(), data.end());
avg = std::accumulate(data.begin(), data.end(), 0.0) / data.size();
}
Rozwiązanie 2 – użycie std::tuple
std::tuple<int, int, double> calculate_stats(const vector<int>& data)
{
auto [min_pos, max_pos] = std::minmax_element(begin(data), end(data));
auto avg = std::accumulate(begin(data), end(data), 0.0) / std::size(data);
return tuple(*min_it, *max_it, avg);
}
// Przykładowe użycie
std::vector data = { 5, 1, 35, 321, 23, 5, 9, 88, 44, 324 };
auto [min, max, avg] = calculate_stats(data);
Konstruowanie krotek¶
Konstrukcja obiektu typu std::tuple
wymaga deklaracji typu i (opcjonalnego) udostępnienia listy wartości początkowych, zgodnych co do typu z typami poszczególnych elementów montowanej krotki.
Konstruktor domyślny krotki inicjalizuje wartościowo każdy element (value initialized).
tuple<int, double, string> triple(42, 3.14, "Test krotki");
tuple<short, string> another; // default value for every element
Funkcja make_tuple(wart1, wart2, ...)
tworzy krotkę, dedukując typy elementów na podstawie typów argumentów wywołania.
tuple<int, double> get_values()
{
return std::make_tuple(3, 5.56);
}
Od C++17 możemy wykorzystać CTAD:
tuple tpl{1, 3.14, "text"s };
Krotki z referencjami¶
Funkcja make_tuple()
określa domyślnie typy elementów jako modyfikowalne i niereferencyjne.
void foo(const A& a, B& b)
{
//...
make_tuple(a, b); // tworzy tuple<A, B>
//...
}
Aby elementy krotki były referencjami należy skorzystać z wrapperów std::ref() oraz std::cref()
A a; B b; const A ca = a;
make_tuple(cref(a), b); // tworzy tuple<const A&, B>
make_tuple(ref(a), b); // tworzy tuple<A&, B>
make_tuple(ref(a), cref(b)); // tworzy tuple<A&, const B&>
make_tuple(cref(ca)); // tworzy tuple<const A&>
make_tuple(ref(ca)); // tworzy tuple<const A&>
Odwołania do elementów krotek¶
Elementy krotki są dostępne przy pomocy zewnętrznej funkcji get<Index>()
zwracającej referencję do odpowiedniego elementu krotki.
double d = 2.7; A a;
tuple<int, double&, const A&> t(1, d, a);
const tuple<int, double&, const A&> ct = t;
int i = get<0>(t);
int j = get<0>(ct); // ok
get<0>(t) = 5; // ok
get<0>(ct) = 5; // błąd! nie można przypisać do const
double e = get<1>(t); // ok
get<1>(t) = 3.14; // ok
get<2>(t) = A(); // błąd! nie można przypisać do const
A aa = get<3>(t); // błąd! indeks poza zakresem ...
++get<0>(t); // ok, można używać jak zmiennej
Przypisywanie i kopiowanie krotek¶
Krotki można przypisywać i kopiować (wywołując konstruktor kopiujący) pod warunkiem, że dla odpowiednich typów krotki źródłowej i docelowej dostępne są wymagane konwersje.
class A {};
class B : public A {};
struct C { C(); C(const B&); };
struct D { operator C() const; };
...
tuple<char, B*, B, D> t;
...
tuple<int, A*, C, C> a(t); // ok
a = t; // ok
Porównywanie krotek¶
Krotki redukują operatory ==
, !=
, <
, >
, <=
i >=
do odpowiadających im operatorów typów przechowywanych w krotce.
Równość pomiędzy krotkami a i b jest zdefiniowana przez:
a == b
jeśli dla każdegoi
:a[i] == b[i]
a != b
jeśli istniejei
:a[i] != b[i]
Operatory <
, >
, <=
oraz >=
implementują porównywanie leksykograficzne.
tuple<string, int, A> t1("same?", 2, A());
tuple<string, long, A> t2("same?", 2, A());
t1 == t2; // true
Wiązanie zmiennych w krotki¶
Funkcja szablonowa std::tie()
umożliwia wiązanie samodzielnych zmiennych w krotki. Wszystkie elementy krotki utworzonej przez std::tie
są modyfikowalnymi referencjami.
std::vector data = { 5, 1, 35, 321, 23, 5, 9, 88, 44, 324 };
int min, max;
double avg;
std::tie(min, max, avg) = calculate_stats(data);
cout << "Min: " << min << "; Max: " << max << "; Avg: " << avg << endl;
Wyjście:
1 324 85.5
Mechanizm wiązania działa również z obiektami szablonu std::pair<T>
.
Obiekt std::ignore
umożliwia ignorowanie elementu krotki przy operacji wiązania zmiennych
int min, max;
std::tie(min, max, ignore) = calculate_stats(data);
Klasa std::any¶
std::any
umożliwia:
- bezpieczne (typowane) mechanizmy przechowywania i odwoływania się do wartości dowolnych typów
- bezpiecznie typizowany odpowiednik
void*
- bezpiecznie typizowany odpowiednik
- przechowywanie elementów heterogenicznych w kontenerach biblioteki standardowej
- przekazywanie wartości dowolnych typów pomiędzy warstwami, bez konieczności wyposażania warstw pośredniczących w jakąkolwiek wiedzę o tych typach
Klasa std::any
służy do przechowywania i udostępniania wartości dowolnych typów, ale pod warunkiem znajomości tych typów, a więc z zachowaniem zalet i bezpieczeństwa typowania.
Typy przechowywane w std::any
muszą spełniać następujące warunki:
- muszą umożliwiać kopiowanie - typy move-only nie są wspierane, choć klasa
- muszą umożliwiać przypisywanie (publiczny operator przypisania)
- nie mogą rzucać wyjątków z destruktora (to jest wymóg odnośnie wszystkich typów użytkownika w C++)
Interfejs klasy std::any¶
-
any
() domyślny konstruktor, tworzący pusty egzemplarz obiektu klasy
any
-
any
(const any &other) konstruktor kopiujący
-
any
(any &&other) konstruktor przenoszący
-
template<typename
ValueType
>any
(ValueType &&value) szablonowa wersja konstruktora do tworzenia obiektu przechowywującego kopię argumentu typu
ValueType
-
template<typename
ValueType
, class ...Args
>
std::decay_t<ValueType> &emplace
(Args&&... args) Zmienia przechowywany obiekt na nowy typu
ValueType
konstruowany z argumentówargs
-
void
swap
(any &other) wymienia wartości przechowywane pomiędzy dwoma obiektami klasy
any
-
any &
operator=
(const any &other) jeśli obiekt nie jest pusty, operator przypisania powoduje usunięcie przechowywanej wartości i przyjęcie kopii wartości przechowywanej w
other
-
template<typename
ValueType
>
any &operator=
(ValueType &&value) szablonowa wersja operatora przypisania
-
bool
has_value
() const sygnalizuje stan egzemplarza
any
, zwracająctrue
, jeśli egzemplarz przechowuje jakąkolwiek wartość
-
const std::type_info &
type
() const opisuje typ przechowywanej wartości
-
void
reset
() jeśli obiekt nie jest pusty, przechowywany obiekt jest niszczony
Funkcje zewnętrzne¶
Dwie wersje funkcji szablonowej any_cast
:
-
template<typename
ValueType
>
ValueTypeany_cast
(const any &operand) funkcja
any_cast
udostępnia wartość przechowywaną w obiekcieany
. Argumentem wywołania jest obiektany
, którego wartość ma zostać wyłuskana. Jeśli parametr szablonu funkcjiValueType
nie odpowiada właściwemu typowi przechowywanego elementu rzucany jest wyjątekstd::bad_any_cast
-
template<typename
ValueType
>
ValueType *any_cast
(any *operand) przeciążona wersja
any_cast
, przyjmująca wskaźniki obiektów i zwracająca typowane wskaźniki wartości przechowywanych wany
. Jeśli typValueType
nie odpowiada typowi właściwemu typowi wartości przechowywanej, zwracany jest wskaźnik pusty (nullptr
).
Stosowanie std::any¶
std::any a;
a = std::string("Tekst...");
a = 42;
a = 3.1415;
double pi = std::any_cast<double>(a); // OK
std::string s = std::any_cast<std::string>(a); // rzuca wyjatek std::bad_any_cast
Gadget* g = std::any_cast<Gadget>(&a); // zwraca nullptr
if (g)
g->do_stuff();
else
std::cout << "Niepoprawna konwersja dla obiektu any.\n";
Kontenery heterogeniczne z std::any¶
Klasa std::any
umożliwia przechowywanie w kontenerach standardowych elementów różnych, niezwiązanych ze sobą typów.
void print_any(const std::any& a)
{
// ...
}
std::vector<std::any> store_anything;
store_anything.push_back(A());
store_anything.push_back(B());
store_anything.push_back(C());
store_anything.push_back(Gadget{"ipad"});
//...
for(const auto& obj : store_anything)
print_any(obj);
Obiekty typu std::any w algorytmach standardowych¶
Algorytmy standardowe mogą być wykonywane na heterogenicznych kontenerach zawierających obiekty typu any
.
using namespace std;
// predykat
auto is_int = [](const std::any& a) { return typeid(int) == a.type(); };
vector<std::any> a = { 1, 3.14, "text"s, 42, 44.4f, 665 };
vector<std::any> b;
copy_if(a.begin(), a.end(), back_inserter(b), is_int);
Klasa std::string_view¶
Nagłówek:
<string_view>
Lekki uchwyt dla sekwencji znaków (read-only)
czas życia danych (bufora znaków) nie jest kontrolowany przez obiekt typu
string_view
string_view good("text literal"); // OK - internal pointer points to static array string_view bad("string literal"s); // BAD - internal pointer is a dangling pointer
brak wsparcia dla alokatorów - nie są potrzebne
przekazywanie przez wartość jest efektywne
typowa implementacja: wskaźnik na stały znak (
const char*
) i rozmiar
Zdefiniowane są również odpowiedniki dla innych typów znakowych niż
char
:std::wstring_view
- dla typuwchar_t
std::u16string_view
- dla typuchar16_t
std::u32string_view
- dla typuchar32_t
Literał:
sv
- zdefiniowany w nagłówku
<literals>
- zdefiniowany jako
constexpr
auto txt = "text"sv;
- zdefiniowany w nagłówku
Obiekt
string_view
zapewnia podobną funkcjonalność jakstd::string
:operator[]
at()
data()
size()
length()
find()
find_first_of()
find_last_of()
Zapewnia operatory porównania i wyliczania skrótu (
std::hash<std::string_view>
)
Różnice między string_view a string¶
Wartość po konstrukcji domyślnej dla wewnętrznego wskaźnika to
nullptr
string::data
nie może zwrócićnullptr
string_view txt; assert(txt.data() == nullptr); assert(txt.size() == 0);
Typ
string_view
nie ma gwarancji, że bufor znaków jest zakończony zerem (null terminated string)char txt[3] = { 't', 'x', 't' }; string_view txt_v(txt, sizeof(txt)); // this view is not null terminated
Ostrzeżenie
Dla string_view
zawsze należy sprawdzić rozmiar operacją size()
zanim użyty zostanie operator[]
lub wywołana zostanie metoda data()
Konwersje string <->
string_view¶
- Konwersja
string -> string_view
jest szybka- dozwolona niejawna konwersja przy pomocy
std::string::operator string_view()
- dozwolona niejawna konwersja przy pomocy
- Konwersja
string_view -> string
jest kosztowna- wymagana jawna konwersja -
explicit std::string::string(string_view sv)
- wymagana jawna konwersja -
Użycie string_view w API funkcji¶
- Obiekty
string_view
przekazywane jako argumenty wywołania funkcji powinny być przekazywane przez wartość.
void foo_s(const string& s);
void foo_sv(string_view sv);
foo_s("text"); // computes length, allocates memory, copies characters
foo_sv("text"); // computes only length
string_view
powinno być stosowane zamiaststring
jeśli:- API nie wymaga, aby tekst był zakończony zerem
- nie można przekazywać
string_view
do funkcji języka C
- nie można przekazywać
- odbiorca respektuje czas życia obiektu
- dostęp do danych przy pomocy metody
data()
uwzględnia potencjalny pusty wskaźnik (nullptr
)
- API nie wymaga, aby tekst był zakończony zerem
Należy unikać zwracania
string_view
, chyba że jest to świadomy wybór programistyzwrócenie
string_view
może być niebezpieczne - należy pamiętać o tym, żestring_view
jest non-owning viewstring_view start_from_word(string_view text, string_view word) { return text.substr(text.find(word)); }
Jeśli wywołamy funkcję
start_from_word()
w następujący sposób:auto text = "one two three"s; auto sv = start_from_word(text + " four", "two");
Dostaniemy instancję
string_view
z wiszącym wskaźnikiem odnoszącym się do nieaktualnej już tablicy znaków, która została zwolniona w momencie wyjścia z funkcji.
Dostarczanie obydwu wersji funkcji jako przeciążeń może powodować dwuznaczności:
void foo(const string& s); void foo(string_view sv); foo("ambigous"); // ERROR - ambigous call
Klasa std::optional¶
- Nagłówek:
<optional>
Obiekt typu std::optional
opcjonalnie przechowuje wartość określonego typu - jest pusty lub posiada określoną wartość.
W rezultacie nie ma potrzeby korzystać ze specjalnych znaczników pustej wartości (np. NULL, -1, itp.)
Tag pomocniczy - std::nullopt¶
Klasa std::optional
wykorzystuje stałą std::nullopt
typu std::nullopt_t
jako specjalny znacznik oznaczający brak wartości dla obiektu.
inline constexpr nullopt_t nullopt{ /*unspecified*/ };
Konstruktory¶
Obiekt std::optional
może zostać skonstruowany:
w stanie be wartości:
std::optional<std::string> o1; std::optional<double> o2 = std::nullopt;
z określoną wartością
std::optional<std::string> o3 = "text"; std::optional o4{42}; // deduces optional<int>
in-place na podstawie listy argumentów - bez konieczności tworzenia obiektu tymczasowego
std::optional<std::complex<double>> o5{std::in_place, 3.0, 4.0}; // initialize set with lambda as sorting criterion: auto sc = [] (int x, int y) { return std::abs(x) < std::abs(y); }; std::optional<std::set<int, decltype(sc)>> o6{std::in_place, {4, 8, -7, -2, 0, 5}, sc};
przy pomocy funkcji pomocniczej
std::make_optional()
auto o7 = std::make_optional(3.0); // optional<double>
Sprawdzenie stanu¶
Aby sprawdzić, czy obiekt opcjonalny przychowuje wartość możemy użyć:
- metody
has_value()
- przeciążonej funkcji
operator bool
std::optional o{42};
assert(o.has_value() == true);
if (o) // has value
{
//...
}
if (!o) // is empty
{
//...
}
Dostęp do przechowywanej wartości¶
Unsafe¶
Dostęp do przechowywanej wartości zapewniony jest poprzez przeciążenie operatorów dereferencji *
oraz *->
:
*opt_str = "other";
assert(opt_str.value() == "other");
assert(opt_str->length() == 5);
Ostrzeżenie
Użycie tych operatorów w sytuacji, gdy obiekt jest pusty (nie przechowuje wartości) skutkuje undefined behavior
Safe¶
Bezpieczny dostęp do przechowywanej wartości może być zrealizowany poprzez metody:
-
const T &
value
() zwraca wartość. Jeśli jej nie ma rzuca wyjątkiem
std::bad_optional_access
std::optional<std::string> opt_str;
try
{
string str = opt_str.value();
}
catch(const std::bad_optional_access& e)
{
//...
}
-
template<typename
U
>
Tvalue_or
(U &&default_value) zwraca wartość lub jeśli jej nie ma, podaną jako argument wartość domyślną
#include <optional>
#include <iostream>
#include <cstdlib>
std::optional<const char*> maybe_getenv(const char* n)
{
if(const char* x = std::getenv(n))
return x;
else
return {};
}
//...
std::cout << maybe_getenv("MYPWD").value_or("(none)") << '\n';
Resetowanie stanu¶
Usunięcie wartości realizowane jest za pomocą metody reset()
.
Semantyka przenoszenia¶
Klasa std::optional
wspiera semantykę przenoszenia:
std::optional<std::string> os;
std::string text = "text";
os = std::move(text); // OK - string object is moved to optional
std::string destination = std::move(*os);
Ostatnia instrukcja w powyższym przykładzie pozostawia obiekt std::optional
z wartością (os.has_value() == true
), ale w nieokreślonym stanie.
Specjalne przypadki¶
W przypadku zmiennych typu std::optional
przechowywanie w nich wartości typu bool
i wskaźników może mieć zaskakujące efekty.
std::optional<bool>¶
std::optional<bool> o{false};
if (!o) // yields false - o has value, which is false
{
//...
}
if (o == false) // yields true
{
}
std::optional<T*>¶
std::optional<double*> o{nullptr};
if (!o) // yields false - o has value
{
//...
}
if (o == nullptr) // yields true
{
//...
}
Case Study¶
Opcjonalne składowe klasy¶
class Person
{
std::string first_name_;
std::optional<std::string> middle_name_;
std::string last_name_;
public:
Person(std::string fn, std::optional<std::string> mn, std::string ln)
: first_name_{std::move(fn)}, middle_name_{std::move(mn)}, last_name_{std::move(ln)}
{}
std::string full_name() const
{
return first_name_ + " " + ( middle_name_ ? *middle_name_ + " " : "") + last_name_;
}
};
//...
Person p1{"Jan", "Maria", "Kowalski"};
assert(p1.full_name() == "Jan Maria Kowalski");
Person p2{"Jan", std::nullopt, "Kowalski"};
assert(p2.full_name() == "Jan Kowalski");
Klasa std::variant¶
Typ wariantowy w C++17:
- umożliwia bezpieczne (ze względu na typy) przechowanie wartości określonego typu, wybranego z listy typów definiujących zmienną wariantową
std::variant
jest implementacją koncepcji bezpiecznej unii (type-safe union)
- umożliwia statyczny podgląd (wizytację) zmiennych wariantowych
- efektywnie składuje wartości z listy typów wariantowych na stosie
- nie może dynamicznie alokować pamięci na stercie (istotna różnica w stosunku do
std::any
)
- nie może dynamicznie alokować pamięci na stercie (istotna różnica w stosunku do
- nie może przechowywać referencji, tablic oraz typu
void
- może zawierać duplikaty typów na liście
- obsługa takiego przypadku jest realizowana poprzez indeksy (tak jak w
std::tuple
)
- obsługa takiego przypadku jest realizowana poprzez indeksy (tak jak w
Unie w C++ mogą przechowywać tylko typy POD:
union
{
int i; double d;
} u;
u.d = 3.14;
u.i = 3; // nadpisuje u.d (OK: u.d jest typem POD)
Są zatem bezużyteczne w programowaniu obiektowym:
union
{
int i;
std::string s; // błąd kompilacji: std::string nie jest typem POD
} u;
Alternatywne rozwiązania:
- użycie
void*
- jest niebezpieczne typologicznie std::any
- rozwiązanie mało wydajne
- dodanie nowego typu może spowodować błędy
Stosowanie std::variant¶
Plik nagłówkowy: <variant>
Konstrukcja zmiennej wariantowej¶
Deklarując typ wariantowy std::variant
trzeba podać zestaw typów, które będą mogły być reprezentowane w typie wariantowym.
std::variant<int, string, double> my_variant1; // holds an int with a default value 0
std::variant<int, string, double> my_variant2(3.14); // holds a double 3.14
my_variant1 = 24;
my_variant1 = 2.52;
my_variant1 = "text"s;
Domyślny konstruktor typu wariantowego inicjuje zmienną domyślną wartością dla pierwszego typu z listy.
Jeśli pierwszy typ z listy nie jest domyślnie konstruowalny, można użyć tagu - std::monostate
:
struct S
{
S(int v) : value{v}
{}
int value;
};
std::variant<S, int> v1; // ERROR - ill-formed
std::variant<monostate, S, int> v2; // OK - now v2 must be assigned
Konstruktor typu wariantowego może przeprowadzać konwersje, co może dać zaskakujący efekt:
variant<string, bool> x("abc"); // OK, but chooses bool
Przypisania wartości do zmiennej wariantowej¶
Przypisanie nowej wartości dla zmiennej wariantowej możemy zrealizować na dwa sposoby:
-
template<typename
T
>
variant &operator=
(T &&x)¶ variant<int, string, double> v1; v1 = 42; // v1 holds int{42} v1 = "text"s; // v1 holds "text"s v1 = 3.14; // v1 holds double{3.14} variant<int, string, string> v2; v2 = "text"s; // ERROR variant<string, bool> v3; v3 = "ctext"; // v3 holds bool{true}
-
template<typename
T
, typename ...Args
>
T &emplace
(Args&&... args)¶ Tworzy nową wartość (in-place) w istniejącej zmiennej wariantowej. Jest jedyną możliwością przypisania wartości dla duplikatów typu na liście.
class Gadget
{
int id_;
std::string name_;
Gadget(int id, const std::string& name)
: id_{id}, name_{name}
{}
//...
};
variant<int, Gadget, int> v;
v.emplace<Gadget>(1, "ipad"); // creates Gadget{1, "ipad"} inside variant object
v.emplace<0>(42); // sets the first int to 42
v.emplace<2>(665); // sets the second int to 665
Dostęp do wartości przechowywanej w zmiennej wariantowej¶
Bezpieczny dostęp do wartości przechowywanej w zmiennej wariantowej odbywa się przy pomocy funkcji std::get<T>(v)
lub std::get<Index>(v)
.
Jeśli wywołanie get(v)
okaże się nieskuteczne (zmienna wariantowa zawiera wartość typu różnego od argumentu szablonu funkcji get(v)
), zgłaszany jest wyjątek std::bad_variant_access
.
Aby uniknąć zgłaszania niepowodzenia w postaci wyjątku, należy wywołać funkcję std::get_if(v)
przekazując jako argument wskaźnik do zmiennej wariantowej. W razie niezgodności typów, zwrócony zostanie nullptr
.
std::variant<int, std::string, double> my_variant{"text"s};
std::string s1 = std::get<std::string>(my_variant); // OK
std::string s2 = std::get<1>(my_variant); // OK
std::get<string>(v) += "!!!";
int x = std::get<int>(my_variant); // ERROR - throws std::bad_variant_access
if (std::string* ptr_str = std::get_if<std::string>(&my_variant); ptr_str != nullptr)
cout << "Stored string: " << *ptr_str << endl;
Inne funkcje API dla klasy std::variant¶
-
std::size_t
std::variant
::
index
() const¶ Zwraca indeks (licząc od zera) typu z listy dla danego stanu zmiennej wariantowej.
std::variant<int, double> v = 3.14; assert(v.index() == 1);
-
template<typename
T
>
boolholds_alternative
(const std::variant &v)¶ Sprawdza, czy zmienna wariantowa przechowuje w danym momencie odpowiedni typ.
if (std::holds_alternative<double>(v)) std::cout << "Holds double\n";
Problem pustego stanu¶
Obiekt klasy std::variant
może stać się pustym obiektem tylko w przypadku wystąpienia wyjątku w trakcie operacji przypisania nowej (lub domyślnej) wartości.
W takim przypadku:
- metoda
valueless_by_exception()
zwracatrue
- a wywołanie metody
index()
zwraca wartośćstd::variant_npos
struct S
{
operator int()
{
throw std::runtime_error("ERROR#13");
}
};
std::variant<int, double> v{3.14};
try
{
v.emplace<0>(S{}); // throws while being set
}
catch(...)
{
assert(v.valueless_by_exception() == true);
assert(v.index() == std::variant_npos);
}
Wizytowanie wariantów¶
Rozwiązaniem problemu przeglądania wartości przechowywanych w zmiennych wariantowych jest zastosowanie wizytatora. Wizytatory są implementowane jako obiekty funkcyjne z operatorami wywołania funkcji przyjmującymi argumenty typów odpowiadających typom z zestawu wariantowego.
Wizytacja odbywa się za pośrednictwem funkcji std::visit(wizytator, zmienna-wariantowa)
. Jeżeli zmieni się zestaw typów w zmiennej wariantowej, która była wizytowana przez wizytatora i wizytator nie będzie w stanie obsłużyć nowo dodanego typu, to kompilator zgłosi błąd.
Klasa wizytatora¶
Klasa wizytatora:
class PrintVisitor
{
public:
void operator()(int i) const
{
cout << "int: " << i << "\n";
}
void operator()(string s) const
{
cout << "string: " << s << "\n";
}
};
std::variant<int, string> var("Test"s);
PrintVisitor pv;
std::visit(pv, var); // compile error if type is not supported by visitor
var = 12;
std::visit(PrintVisitor{}, var);
Wizytacja za pomocą lambd¶
Do wizytacji można również wykorzystać lambdę generyczną:
std::visit([](auto&& value) { std::cout << value << "\n" }, var);
Istnieje możliwość zbudowania wizytora w miejscu wizytacji (in-place). Do tego celu będziemy potrzebowali
funkcji wariadycznej make_inline_visitor()
, której implementacja korzysta z variadic templates.
Funkcja ta pozwoli utworzyć obiekt funkcyjny składający się z przeciążonych operatorów wywołania funkcji, który zostanie wykorzystany do wizytacji zmiennej wariantowej.
template <typename... Ts>
struct Overloader : Ts...
{
using Ts::operator()...;
};
template <typename... Ts>
auto make_inline_visitor(Ts&&...op)
{
return Overloader<Ts...>{op...};
}
variant<int, double, string> v = 42;
auto local_visitor =
make_inline_visitor(
[](int value) { return "int: "s + to_string(value); },
[](double value) { return "double: "s + to_string(value); },
[](const string s) { return "string: " + s; }
);
auto result = visit(local_visitor, v);
assert(result == "int: 42"s);
v = text;
result = visit(local_visitor, v);
assert(result == "string: text");