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

4 SFINAE, koncepty a type erasure

Po přečtení umíš
  • omezit šablonu pomocí requires a poznat starší enable_if
  • porovnat statický a dynamický polymorfizmus
  • vysvětlit princip type erasure
  • použít CRTP

4.1 Ukázka: omezení šablony

Chceme funkci jen pro čísla s plovoucí řádovou čárkou. Moderní zápis je koncept:

#include <cassert>
#include <cmath>
#include <concepts>

template <std::floating_point T>
bool almost_equal(T a, T b) {
  return std::abs(a - b) < T(1e-6);
}

template <typename T>
concept Comparable = requires(T value) { almost_equal(value, value); };

static_assert(Comparable<double>);
static_assert(!Comparable<int>);

int main() {
  assert(almost_equal(1.0, 1.0));
}

Pro int překladač řekne, že podmínka není splněná, místo stovek řádků chyb uvnitř šablony.

4.2 SFINAE

Definice 4.2 (SFINAE)

Substitution Failure Is Not An Error: selže-li dosazení šablonových argumentů v deklaraci, šablona se tiše vyřadí z kandidátů a překlad pokračuje.

Před C++20 se toho využívalo přes std::enable_if:

#include <type_traits>

template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
constexpr int kind(T) { return 1; }

template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
constexpr int kind(T) { return 2; }

static_assert(kind(1) == 1);
static_assert(kind(1.0) == 2);

Dnes stejnou věc napíšeš přes requires. Starší kód ale SFINAE potkáš, proto je na něj dobré umět odpovědět.

4.3 Vlastní koncept

#include <concepts>
#include <cstddef>
#include <string>
#include <vector>

template <typename T>
concept Sized = requires(const T& value) {
  { value.size() } -> std::convertible_to<std::size_t>;
};

static_assert(Sized<std::string>);
static_assert(Sized<std::vector<int>>);
static_assert(!Sized<int>);

4.4 Statický a dynamický polymorfizmus

Šablony Virtuální funkce
Rozhodnutí při překladu za běhu
Režie žádná, inlining nepřímé volání
Kód roste s počtem typů jedna implementace
Heterogenní kolekce ne ano
Chyby dlouhé hlášky jasné

4.5 Type erasure

Definice 4.3 (Type erasure)

Skrytí konkrétního typu za jednotné rozhraní, aniž by typ musel dědit od společné třídy. Vnitřně je to šablonový potomek abstraktní třídy.

std::any, std::function a shared_ptr s deleterem to používají. Malá verze:

#include <cassert>
#include <memory>
#include <string>
#include <vector>

class Printable {
  struct Base {
    virtual ~Base() = default;
    virtual std::string text() const = 0;
  };
  template <typename T>
  struct Model : Base {
    T value;
    explicit Model(T v) : value(std::move(v)) {}
    std::string text() const override { return std::to_string(value); }
  };
  std::unique_ptr<Base> held_;

public:
  template <typename T>
  explicit Printable(T value) : held_(std::make_unique<Model<T>>(std::move(value))) {}
  std::string text() const { return held_->text(); }
};

int main() {
  std::vector<Printable> items;
  items.emplace_back(1);
  items.emplace_back(2.5);
  assert(items[0].text() == "1");
  assert(items[1].text().starts_with("2.5"));
}

4.6 CRTP

Potomek předá sám sebe jako parametr předka. Předek volá jeho metody bez virtuálních funkcí.

#include <cassert>

template <typename Derived>
struct Countable {
  int doubled() const { return 2 * static_cast<const Derived*>(this)->value(); }
};

struct Five : Countable<Five> {
  int value() const { return 5; }
};

int main() {
  assert(Five{}.doubled() == 10);
}

4.7 Typické chyby

4.8 Kvíz

Kvíz

1. Kdy se uplatní SFINAE?
2. Co je std::function?
3. Co je výhodou šablon proti virtuálním funkcím? Může být správně více možností, ale také žádná.
4. Vysvětli, proč type erasure potřebuje virtuální funkce, když je to šablona.
Ukázat řešeníKonstruktor je šablona a zná typ T jen v okamžiku vzniku. Pozdější volání typ nezná, proto ho nese potomek Model a metoda se volá přes abstraktní Base.

4.9 Příklady na procvičení

Cvičení 4.4

Napiš koncept Addable pro typ, který jde sečíst se sebou samým.

Řešení
#include <concepts>
#include <string>

template <typename T>
concept Addable = requires(T a, T b) {
  { a + b } -> std::convertible_to<T>;
};

struct Empty {};
static_assert(Addable<int>);
static_assert(Addable<std::string>);
static_assert(!Addable<Empty>);
Cvičení 4.5

Kdy zvolíš std::variant a kdy type erasure?

Řešení

std::variant, když znáš všechny typy předem: bez alokace a bez virtuálních volání. Type erasure, když typy nejsou předem známé a dodá je uživatel.

4.10 Tahák

Tahák
  • Koncepty a requires lepší než enable_if.
  • SFINAE: jen deklarace, tělo nechrání.
  • Šablona: překlad, rychlé, kód roste.
  • Virtuální: běh, jedna implementace, kolekce.
  • Type erasure: šablonový konstruktor + abstraktní Base + Model.
  • CRTP: statický polymorfizmus přes Derived v šabloně.

Zdroje

  • ni_epc_09: SFINAE strany 3–10, koncepty 11–15, výhody a nevýhody šablon 16–24, type erasure a std::any 25–46, klonování 47–53, CRTP dále v téže přednášce