Přeskočit na obsah

Šablony Metaprogramovací vzory

Zvládněte pokročilé vzory šablon: CRTP pro statický polymorfismus, návrh založený na zásadách pro flexibilní kompozici, mazání typů pro generika za běhu a pokyny, kdy používat šablony vs virtuální odesílání.

Nad rámec základních šablon

Šablony umožňují návrhové vzory, které nemají ekvivalent v jiných jazycích. Tyto vzory využívají povahu šablon v době kompilace k dosažení statického polymorfismu (žádná režie virtuálních funkcí), složitelného chování (zásady) a typově bezpečného vymazání (skrytí konkrétních typů za jednotné rozhraní).

Toto nejsou vzory pro začátečníky – jsou to nástroje pro autory knihoven a návrháře frameworků. Jejich pochopení je nezbytné pro čtení standardních interních knihoven, Boost a moderních frameworků C++.

CRTP: Kuriózně se opakující vzor šablony

V CRTP se třída odvozuje ze šablony vytvořené s sebou jako argumentem:class Derived : public Base. Základní třída může volat metody na odvozené třídě v době kompilace – žádná režie virtuálního odeslání. To se používá k injektování chování, implementaci statického polymorfismu a vytvoření mixin funkčnosti.

crtp.cpp
#include <iostream>

// CRTP base: provides a counter for each derived class
template <typename Derived>
class Counter {
    static inline int count_ = 0;

protected:
    Counter()  { ++count_; }
    ~Counter() { --count_; }
    Counter(const Counter&) { ++count_; }

public:
    static int alive() { return count_; }
};

// Each derived class gets its OWN counter
class Dog : public Counter<Dog> {
public:
    std::string name;
    Dog(std::string n) : name(std::move(n)) {}
};

class Cat : public Counter<Cat> {
public:
    std::string name;
    Cat(std::string n) : name(std::move(n)) {}
};

// CRTP for static polymorphism: no virtual dispatch
template <typename Derived>
class Shape {
public:
    double area() const {
        // Call derived class method — resolved at compile time
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Circle : public Shape<Circle> {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double area_impl() const { return 3.14159265 * radius_ * radius_; }
};

class Square : public Shape<Square> {
    double side_;
public:
    Square(double s) : side_(s) {}
    double area_impl() const { return side_ * side_; }
};

int main() {
    Dog d1("Rex"), d2("Buddy");
    Cat c1("Whiskers");
    std::cout << "Dogs alive: " << Dog::alive() << '\n'; // 2
    std::cout << "Cats alive: " << Cat::alive() << '\n'; // 1

    Circle circle(5.0);
    Square square(4.0);
    std::cout << "Circle area: " << circle.area() << '\n'; // 78.54
    std::cout << "Square area: " << square.area() << '\n'; // 16.0
}

Návrh založený na zásadách

Návrh založený na zásadách rozkládá třídu na zaměnitelné komponenty chování (zásady) předávané jako parametry šablony. Každá politika implementuje jeden aspekt chování. Uživatelé si skládají přesnou kombinaci, kterou potřebují. Toto bylo popularizováno Andrei Alexandrescu "Moderní design C++."

policy_design.cpp
#include <iostream>
#include <mutex>
#include <stdexcept>

// Threading policies
struct SingleThreaded {
    struct Lock { Lock(SingleThreaded&) {} }; // no-op
};

struct MultiThreaded {
    std::mutex mtx;
    struct Lock {
        std::lock_guard<std::mutex> guard;
        Lock(MultiThreaded& host) : guard(host.mtx) {}
    };
};

// Bounds checking policies
struct NoBoundsCheck {
    static void check(int /*index*/, int /*size*/) {} // no-op
};

struct StrictBoundsCheck {
    static void check(int index, int size) {
        if (index < 0 || index >= size)
            throw std::out_of_range("Index out of bounds");
    }
};

// Policy-based container: users choose their combination
template <typename T,
         typename ThreadingPolicy = SingleThreaded,
         typename BoundsPolicy = NoBoundsCheck>
class SmartArray : private ThreadingPolicy {
    T* data_;
    int size_;

public:
    SmartArray(int size) : data_(new T[size]{}), size_(size) {}
    ~SmartArray() { delete[] data_; }

    T& operator[](int index) {
        typename ThreadingPolicy::Lock lock(*this);
        BoundsPolicy::check(index, size_);
        return data_[index];
    }

    int size() const { return size_; }
};

int main() {
    // Fast, no overhead: no locking, no bounds checks
    SmartArray<int> fast(10);
    fast[0] = 42;

    // Safe: bounds-checked, single-threaded
    SmartArray<int, SingleThreaded, StrictBoundsCheck> safe(10);
    safe[0] = 42;
    try {
        safe[99] = 0; // throws!
    } catch (const std::out_of_range& e) {
        std::cout << e.what() << '\n';
    }

    // Thread-safe with bounds checking
    SmartArray<int, MultiThreaded, StrictBoundsCheck> robust(10);
    robust[5] = 100;
}

Typ Erasure

Vymazání typu skrývá konkrétní typ za jednotné rozhraní a kombinuje flexibilitu šablon s runtime polymorfismem virtuálních funkcí. Klasickým příkladem jestd::function, který pojme jakýkoli callable. Vzor používá tři komponenty: koncept (abstraktní rozhraní), model (obálka šablony, která implementuje rozhraní pro jakýkoli typ) a třídu vnější úchyt.

type_erasure.cpp
#include <iostream>
#include <memory>
#include <string>

// Type-erased "Printable" — can hold any type with a print() method
class Printable {
    // Concept: abstract interface
    struct Concept {
        virtual ~Concept() = default;
        virtual void print(std::ostream& os) const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };

    // Model: wraps any T that supports operator<<
    template <typename T>
    struct Model : Concept {
        T value;
        Model(T v) : value(std::move(v)) {}
        void print(std::ostream& os) const override { os << value; }
        std::unique_ptr<Concept> clone() const override {
            return std::make_unique<Model>(value);
        }
    };

    std::unique_ptr<Concept> ptr_;

public:
    // Constructor: accepts any printable type
    template <typename T>
    Printable(T value) : ptr_(std::make_unique<Model<T>>(std::move(value))) {}

    // Copy support
    Printable(const Printable& other) : ptr_(other.ptr_->clone()) {}
    Printable& operator=(const Printable& other) {
        ptr_ = other.ptr_->clone();
        return *this;
    }
    Printable(Printable&&) = default;
    Printable& operator=(Printable&&) = default;

    friend std::ostream& operator<<(std::ostream& os, const Printable& p) {
        p.ptr_->print(os);
        return os;
    }
};

int main() {
    // Heterogeneous collection — no common base class needed!
    std::vector<Printable> items;
    items.emplace_back(42);
    items.emplace_back(3.14);
    items.emplace_back(std::string("hello"));
    items.emplace_back('X');

    for (const auto& item : items) {
        std::cout << item << '\n';
    }
    // Output: 42, 3.14, hello, X
}

Šablony výrazů (přehled)

Šablony výrazů jsou technikou, kde aritmetické výrazy na objektech vytvářejí strom v době kompilace operací spíše než počítají mezivýsledky. Konečný výsledek je vypočítán v jediném průchodu, bez dočasných objektů.

Například s naivní třídouVectorvytvořía + b + cdva dočasné vektory. Se šablonami výrazů vytvářía + b + codlehčený výrazový objektAdd, Vector>, který počítá prvky na vyžádání – bez nutnosti provizoria.

Tuto techniku ​​používají vysoce výkonné knihovny lineární algebry jako Eigen a Blaze. Strom výrazů je vyhodnocen líně a kompilátor může často vektorizovat výslednou jednoprůchodovou smyčku.

Šablony výrazů jsou složité na správnou implementaci a jsou primárně záležitostí autorů knihoven. Pokud potřebujete maticovou/vektorovou matematiku, použijte raději knihovnu jako Eigen, než abyste sami implementovali šablony výrazů.

Osvědčený postup

Při výběru mezi šablonami (statický polymorfismus) a virtuálními funkcemi (dynamický polymorfismus):

Použít šablony, když:
- Sada typů je známa v době kompilace
- Potřebujete abstrakce s nulovou režií (žádná vtable, žádná nepřímost)
- Chcete, aby kompilátor inline a optimalizoval přes hranice typu
- Musíte pracovat se sémantikou hodnot (žádná alokace haldy)
- Výkon je kritický (těsné smyčky, číselný kód)

Používejte virtuální funkce, když:
- Sada typů je otevřená a určená za běhu (pluginy, uživatelský vstup)
– Potřebujete heterogenní kontejnery (např.vector>)
- Binární velikost je důležitější než rychlost (šablony generují více kódu)
- Chcete stabilní ABI přes hranice sdílených knihoven
- Musíte přidat nové typy bez překompilování stávajícího kódu

Použít typ mazání, když:
- Chcete jednoduchost rozhraní virtuálního odesílání, ale nechcete nutit uživatele, aby dědili z vaší základní třídy
- Potřebujete hodnotovou sémantiku s runtime polymorfismem (std::function,std::any)

Úskalí

Metaprogramování šablon je výkonné, ale má skutečné náklady:

1. Exploze v době kompilace – hluboce vnořené instance šablon mohou způsobit, že doba kompilace bude nepřijatelná. Rekurzivní TMP je obzvláště špatný — preferujte fold výrazy aif constexpr.
2. Chybové zprávy — bez pojmů, chyby šablony vytvářejí stránky s nesrozumitelným textem. Vždy omezte šablony.
3. Ladění – procházení kódem náročným na šablony v debuggeru je bolestivé. Typově vymazané obálky přidávají nepřímé směrování, které skryje skutečný typ.
4. Čitelnost – CRTP, návrh založený na zásadách a šablony výrazů jsou pro začínající vývojáře těžko pochopitelné. Zdokumentujte název vzoru a záměr.
5. Overengineering — ne každý problém potřebuje řešení šablony. Jasnější a dostačující může být jednoduchá virtuální funkce nebo dokonce příkaz switch.

Klíčové poznatky
  • CRTP umožňuje statický polymorfismus a čítače/mixiny podle třídy odvozením zBase
  • Policy-based design decomposes behavior into template parameters — users compose the exact combination they need
  • Vymazání typu kombinuje flexibilitu šablony s polymorfismem běhového prostředí (např.std::function,std::any)
  • Expression templates eliminate temporaries in chained operations — used by Eigen, Blaze, and similar libraries
  • Vyberte šablony pro známé typy a nulovou režii; virtuální odeslání pro sady otevřeného typu a flexibilitu za běhu
  • Avoid overengineering — use the simplest mechanism that meets your requirements

Kvíz — Otestujte své znalosti

(20 XP)

1. In CRTP, what does`class Dog : public Counter<Dog>`achieve?

2. Jaký problém řeší mazání typu?

3. Kdy dát přednost virtuálním funkcím před šablonami?