Čtení: 45 minHodnotí se: zkouška

3 Šablony, dedukce a perfect forwarding

Po přečtení umíš
  • určit, co dedukuje T, const T&, T&&
  • použít specializaci a metafunkce
  • napsat variadickou šablonu
  • vysvětlit std::forward a emplace

3.1 Ukázka: proč šablona v hlavičce

Šablona není kód, ale návod. Kód vznikne až instanciací pro konkrétní typ, takže překladač musí vidět definici. Proto šablony patří do hlavičkových souborů.

#include <cassert>
#include <string>

template <typename T>
T twice(const T& value) {
  return value + value;
}

int main() {
  assert(twice(21) == 42);
  assert(twice(std::string("ha")) == "haha");
}

3.2 Dedukce šablonových argumentů

Parametr Argument T Typ parametru
T const int lhodnota int int
const T& int lhodnota int const int&
T& const int lhodnota const int const int&
T&& int lhodnota int& int&
T&& int rhodnota int int&&

Předání hodnotou zahodí const a referenci. Řádky s T&& jsou speciální: šablonový T&& je forwarding reference.

Definice 3.2 (Kolaps referencí)

& &, & && a && & kolabují na &. Pouze && && dává &&.

#include <type_traits>
#include <utility>

template <typename T>
constexpr bool deduces_reference(T&&) {
  return std::is_lvalue_reference_v<T>;
}

int value = 0;
static_assert(deduces_reference(value));
static_assert(!deduces_reference(std::move(value)));

3.3 Specializace a metafunkce

Definice 3.3 (Metafunkce)

Šablona, která počítá s typy v době překladu. Výsledek je člen type nebo value.

Implementace remove_reference přes částečnou specializaci, na které pak stojí std::move:

#include <cassert>
#include <type_traits>

template <typename T> struct my_remove_reference { using type = T; };
template <typename T> struct my_remove_reference<T&> { using type = T; };
template <typename T> struct my_remove_reference<T&&> { using type = T; };

template <typename T>
constexpr typename my_remove_reference<T>::type&& my_move(T&& value) noexcept {
  return static_cast<typename my_remove_reference<T>::type&&>(value);
}

static_assert(std::is_same_v<my_remove_reference<int&>::type, int>);

int main() {
  int x = 1;
  int&& r = my_move(x);
  r = 2;
  assert(x == 2);
}

3.4 Variadické šablony

Parametr typename... Ts přijme libovolný počet typů. Rozbalení se píše .... Od C++17 je nejkratší fold expression.

#include <cassert>
#include <cstddef>

template <typename... Ts>
constexpr auto sum(Ts... values) {
  return (values + ...);
}

template <typename... Ts>
constexpr std::size_t count() {
  return sizeof...(Ts);
}

static_assert(sum(1, 2, 3) == 6);
static_assert(count<int, char, double>() == 3);

3.5 Perfect forwarding

Obal má předat argument dál bez ztráty kategorie: lhodnotu jako lhodnotu, rhodnotu jako rhodnotu. Bez std::forward by se rhodnota v obalu stala lhodnotou, protože má jméno.

#include <cassert>
#include <string>
#include <utility>

int kind(const std::string&) { return 1; }
int kind(std::string&&) { return 2; }

template <typename T>
int bad(T&& value) { return kind(value); }

template <typename T>
int good(T&& value) { return kind(std::forward<T>(value)); }

int main() {
  std::string s = "text";
  assert(bad(std::move(s)) == 1);   // rhodnota se ztratila
  assert(good(std::move(s)) == 2);
  assert(good(s) == 1);
}

std::move používej tam, kde víš, že jde o rhodnotu. std::forward jen u forwarding reference.

3.6 Emplace

push_back(x) dostane hotový objekt. emplace_back(args...) předá argumenty rovnou konstruktoru v místě prvku, takže odpadne dočasný objekt.

#include <cassert>
#include <string>
#include <utility>
#include <vector>

struct Named {
  std::string name;
  int age;
  Named(std::string n, int a) : name(std::move(n)), age(a) {}
};

int main() {
  std::vector<Named> people;
  people.emplace_back("Ada", 36);
  assert(people[0].age == 36);
}

3.7 Typické chyby

3.8 Kvíz

Kvíz

1. Co je T ve volání f(x) pro template<typename T> void f(T&&) a int x?
2. Proč template<typename T> struct W { void f(T&&); }; není forwarding reference?
3. Co zbude z int& && po kolapsu?
4. Kolik vrátí sizeof...(Ts) pro <int, int, char, void*>?
5. Co je výhodou emplace_back?
6. Jaký je rozdíl mezi std::move a std::forward?
Ukázat řešenístd::move vždy přetypuje na rhodnotu. std::forward<T> zachová původní kategorii podle dedukovaného T.

3.9 Příklady na procvičení

Cvičení 3.4

Co je T pro template <typename T> void f(T value) při volání s const int& r?

Řešení

int. Předání hodnotou zahodí referenci i const.

Cvičení 3.5

Napiš metafunkci my_is_pointer, která pozná ukazatel.

Řešení
template <typename T> struct my_is_pointer { static constexpr bool value = false; };
template <typename T> struct my_is_pointer<T*> { static constexpr bool value = true; };

static_assert(my_is_pointer<int*>::value);
static_assert(!my_is_pointer<int>::value);
Cvičení 3.6

Napiš variadickou funkci all_true(args...).

Řešení
template <typename... Ts>
constexpr bool all_true(Ts... values) {
  return (values && ...);
}

static_assert(all_true(true, true));
static_assert(!all_true(true, false));

3.10 Tahák

Tahák
  • Šablona se instanciuje, definice patří do hlavičky.
  • Hodnotou: const a reference pryč.
  • T&& s dedukcí = forwarding reference, lhodnota dá T = X&.
  • Kolaps: cokoli s & je &.
  • Metafunkce: specializace, ::type, ::value.
  • (args + ...) fold, sizeof...(Ts) počet.
  • std::forward<T>(x) zachová kategorii.
  • emplace_* = konstrukce na místě.

Zdroje

  • ni_epc_07: instanciace strany 10–14, dedukce 15–37, specializace 38–47, metafunkce 48–53
  • ni_epc_08: variadické šablony strany 3–27, perfect forwarding 28–37, příklady 38–53