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.

_images/buckets_.svg

C++11 wprowadza następujące kontenery haszujące

  • zbiory: unordered_set i unordered_multiset
  • mapy: unordered_map i unordered_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>

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 - klucz
    • T - mapowany typ
    • Hash - funktor wyliczający skrót dla klucza
    • Pred - 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 i k2, które są sobie równe std::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 oraz std::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:

  1. 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;
    
  2. 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żdego i: a[i] == b[i]
  • a != b jeśli istnieje i: 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*
  • 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ów args

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

any &operator=(any &&other)

przenosząca wersja operatora przypisania

template<typename ValueType>
any &operator=(ValueType &&value)

szablonowa wersja operatora przypisania

bool has_value() const

sygnalizuje stan egzemplarza any, zwracając true, 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>
ValueType any_cast(const any &operand)

funkcja any_cast udostępnia wartość przechowywaną w obiekcie any. Argumentem wywołania jest obiekt any, którego wartość ma zostać wyłuskana. Jeśli parametr szablonu funkcji ValueType nie odpowiada właściwemu typowi przechowywanego elementu rzucany jest wyjątek std::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 w any. Jeśli typ ValueType 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 typu wchar_t
    • std::u16string_view - dla typu char16_t
    • std::u32string_view - dla typu char32_t
  • Literał: sv

    • zdefiniowany w nagłówku <literals>
    • zdefiniowany jako constexpr
    auto txt = "text"sv;
    
  • Obiekt string_view zapewnia podobną funkcjonalność jak std::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()
  • Konwersja string_view -> string jest kosztowna
    • wymagana jawna konwersja - explicit std::string::string(string_view sv)

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 zamiast string jeśli:

    • API nie wymaga, aby tekst był zakończony zerem
      • nie można przekazywać string_view do funkcji języka C
    • odbiorca respektuje czas życia obiektu
    • dostęp do danych przy pomocy metody data() uwzględnia potencjalny pusty wskaźnik (nullptr)
  • Należy unikać zwracania string_view, chyba że jest to świadomy wybór programisty

    • zwrócenie string_view może być niebezpieczne - należy pamiętać o tym, że string_view jest non-owning view

      string_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>
T value_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 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)

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>
bool holds_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() zwraca true
  • 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");