Sari la conținut

Modele de metaprogramare șablon

Stăpânește modele de șabloane avansate: CRTP pentru polimorfism static, proiectare bazată pe politici pentru o compoziție flexibilă, ștergere tip pentru generice de rulare și linii directoare pentru când să folosești șabloane față de dispecerare virtuală.

Dincolo de șabloane de bază

Șabloanele permit modele de design care nu au echivalent în alte limbi. Aceste modele exploatează natura în timp de compilare a șabloanelor pentru a obține polimorfism static (fără suprasarcină a funcției virtuale), comportament compus (politici) și ștergere sigură de tip (ascunderea tipurilor concrete în spatele unei interfețe uniforme).

Acestea nu sunt modele pentru începători - sunt instrumente pentru autorii de biblioteci și designerii de cadre. Înțelegerea acestora este esențială pentru citirea elementelor interne standard ale bibliotecii, Boost și cadrelor moderne C++.

CRTP: Model șablon recurent în mod curios

În CRTP, o clasă derivă dintr-un șablon instanțiat cu însuși ca argument:class Derived : public Base. Clasa de bază poate apela metode din clasa derivată la timp de compilare - fără suprasarcină virtuală de expediere. Acesta este folosit pentru a injecta comportament, pentru a implementa polimorfismul static și pentru a crea funcționalitatea mixin.

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
}

Proiectare bazată pe politici

Design bazat pe politici descompune o clasă în componente de comportament interschimbabile (politici) transmise ca parametri șablon. Fiecare politică implementează un aspect al comportamentului. Utilizatorii compun combinația exactă de care au nevoie. Acest lucru a fost popularizat de „Designul modern C++” al lui Andrei Alexandrescu.

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;
}

Tip ștergere

Ștergerea tipului ascunde tipul concret în spatele unei interfețe uniforme, combinând flexibilitatea șabloanelor cu polimorfismul de rulare al funcțiilor virtuale. Exemplul clasic estestd::function, care poate conține orice apelabil. Modelul folosește trei componente: un concept (interfață abstractă), un model (înveliș de șablon care implementează interfața pentru orice tip) și o clasă mâner exterior.

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
}

Șabloane de expresie (prezentare generală)

Șabloanele de expresie sunt o tehnică în care expresiile aritmetice pe obiecte construiesc un arboresc în timp de compilare de operații, mai degrabă decât să calculeze rezultate intermediare. Rezultatul final este calculat într-o singură trecere, eliminând obiectele temporare.

De exemplu, cu o clasăVectornaivă,a + b + ccreează doi vectori temporari. Cu șabloane de expresie,a + b + cconstruiește un obiect de expresie ușorAdd, Vector>care calculează elemente la cerere - nu sunt necesare temporare.

Această tehnică este utilizată de bibliotecile de algebră liniară de înaltă performanță precum Eigen și Blaze. Arborele de expresie este evaluat leneș, iar compilatorul poate vectoriza adesea bucla cu o singură trecere rezultată.

Șabloanele de expresie sunt complexe de implementat corect și sunt în primul rând o preocupare a autorului bibliotecii. Dacă aveți nevoie de matematică matrice/vectorală, utilizați o bibliotecă precum Eigen în loc să implementați singur șabloane de expresie.

Bună practică

Când alegeți între șabloane (polimorfism static) și funcții virtuale (polimorfism dinamic):

Folosiți șabloane când:
- Setul de tipuri este cunoscut la momentul compilarii
- Aveți nevoie de abstracții zero-overhead (fără vtable, fără indirecte)
- Doriți ca compilatorul să fie integrat și să optimizeze peste granițele de tip
- Trebuie să lucrați cu semantica valorii (fără alocare heap)
- Performanța este critică (bucle strânse, cod numeric)

Utilizați funcțiile virtuale când:
- Setul de tipuri este deschis și determinat în timpul execuției (plugin-uri, intrare de utilizator)
- Aveți nevoie de containere eterogene (de exemplu,vector>)
- Dimensiunea binară contează mai mult decât viteza (șabloanele generează mai mult cod)
- Doriți ABI stabil peste limitele bibliotecii partajate
- Trebuie să adăugați noi tipuri fără a recompila codul existent

Utilizați ștergerea tipului atunci când:
- Doriți simplitatea interfeței dispecerului virtual, dar nu doriți să forțați utilizatorii să moștenească din clasa dvs. de bază
- Aveți nevoie de semantică valorică cu polimorfism de rulare (std::function,std::any)

Capcană

Template metaprogramming is powerful but has real costs:

1. Compile-time explosion — deeply nested template instantiations can make compile times unacceptable. Recursive TMP is especially bad — prefer fold expressions andif constexpr.
2. Error messages — without concepts, template errors produce pages of incomprehensible text. Always constrain templates.
3. Debugging — stepping through template-heavy code in a debugger is painful. Type-erased wrappers add indirection that hides the actual type.
4. Readability — CRTP, policy-based design, and expression templates are hard for junior developers to understand. Document the pattern name and intent.
5. Overengineering — not every problem needs a template solution. A simple virtual function or even a switch statement may be clearer and sufficient.

Idei cheie
  • CRTP permite polimorfismul static și contoarele/mixinele pe clasă, derivând dinBase
  • Policy-based design decomposes behavior into template parameters — users compose the exact combination they need
  • Type erasure combines template flexibility with runtime polymorphism (e.g.,std::function,std::any)
  • Expression templates eliminate temporaries in chained operations — used by Eigen, Blaze, and similar libraries
  • Alegeți șabloane pentru tipurile cunoscute și zero overhead; dispecerare virtuală pentru seturi de tip deschis și flexibilitate de rulare
  • Avoid overengineering — use the simplest mechanism that meets your requirements

Test — Verifică-ți cunoștințele

(20 XP)

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

2. What problem does type erasure solve?

3. Când ar trebui să preferați funcțiile virtuale decât șabloane?