Μετάβαση στο περιεχόμενο

Μοτίβα μεταπρογραμματισμού προτύπων

Κατακτήστε προηγμένα μοτίβα προτύπων: CRTP για στατικό πολυμορφισμό, σχεδιασμός βάσει πολιτικής για ευέλικτη σύνθεση, διαγραφή τύπων για γενικά στοιχεία χρόνου εκτέλεσης και οδηγίες για το πότε να χρησιμοποιείτε πρότυπα έναντι εικονικής αποστολής.

Πέρα από τα βασικά πρότυπα

Τα πρότυπα ενεργοποιούν μοτίβα σχεδίασης που δεν έχουν αντίστοιχο σε άλλες γλώσσες. Αυτά τα μοτίβα εκμεταλλεύονται τη φύση του χρόνου μεταγλώττισης των προτύπων για να επιτύχουν στατικό πολυμορφισμό (χωρίς επιβάρυνση εικονικής συνάρτησης), συμπεριφορά σύνθεσης (πολιτικές) και ασφαλή κατά τύπο διαγραφή (απόκρυψη τύπων σκυροδέματος πίσω από μια ομοιόμορφη διεπαφή).

Αυτά δεν είναι μοτίβα για αρχάριους - είναι εργαλεία για συγγραφείς βιβλιοθηκών και σχεδιαστές πλαισίων. Η κατανόησή τους είναι απαραίτητη για την ανάγνωση τυπικών εσωτερικών βιβλιοθηκών, Boost και σύγχρονων πλαισίων C++.

CRTP: Περιέργως επαναλαμβανόμενο μοτίβο προτύπου

Στο CRTP, μια κλάση προέρχεται από ένα πρότυπο με το εαυτό ως όρισμα:class Derived : public Base. Η βασική κλάση μπορεί να καλέσει μεθόδους στην προκύπτουσα κλάση κατά το χρόνο μεταγλώττισης — χωρίς επιβάρυνση εικονικής αποστολής. Αυτό χρησιμοποιείται για την ένεση συμπεριφοράς, την εφαρμογή στατικού πολυμορφισμού και τη δημιουργία λειτουργικότητας 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
}

Σχεδιασμός με βάση την πολιτική

Η Σχεδίαση βάσει πολιτικής αποσυνθέτει μια κλάση σε εναλλάξιμα στοιχεία συμπεριφοράς (πολιτικές) που μεταβιβάζονται ως παράμετροι προτύπου. Κάθε πολιτική εφαρμόζει μια πτυχή συμπεριφοράς. Οι χρήστες συνθέτουν τον ακριβή συνδυασμό που χρειάζονται. Αυτό έγινε δημοφιλές από το "Modern C++ Design" του 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;
}

Type Erasure

Το Type erasure κρύβει τον τύπο του σκυροδέματος πίσω από μια ομοιόμορφη διεπαφή, συνδυάζοντας την ευελιξία των προτύπων με τον πολυμορφισμό χρόνου εκτέλεσης εικονικών συναρτήσεων. Το κλασικό παράδειγμα είναι τοstd::function, το οποίο μπορεί να χωρέσει οποιοδήποτε καλούμενο. Το μοτίβο χρησιμοποιεί τρία στοιχεία: μια έννοια (αφηρημένη διεπαφή), ένα μοντέλο (περιτύλιγμα προτύπου που υλοποιεί τη διεπαφή για οποιονδήποτε τύπο) και μια κλάση εξωτερική λαβή.

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
}

Expression Templates (Overview)

Expression templates are a technique where arithmetic expressions on objects build a compile-time tree of operations rather than computing intermediate results. The final result is computed in a single pass, eliminating temporary objects.

For example, with a naiveVectorclass,a + b + ccreates two temporary vectors. With expression templates,a + b + cbuilds a lightweight expression objectAdd, Vector>that computes elements on demand — no temporaries needed.

This technique is used by high-performance linear algebra libraries like Eigen and Blaze. The expression tree is evaluated lazily, and the compiler can often vectorize the resulting single-pass loop.

Expression templates are complex to implement correctly and are primarily a library-author concern. If you need matrix/vector math, use a library like Eigen rather than implementing expression templates yourself.

Βέλτιστη πρακτική

When choosing between templates (static polymorphism) and virtual functions (dynamic polymorphism):

Use templates when:
- The set of types is known at compile time
- You need zero-overhead abstractions (no vtable, no indirection)
- You want the compiler to inline and optimize across type boundaries
- You need to work with value semantics (no heap allocation)
- Performance is critical (tight loops, numerical code)

Use virtual functions when:
- The set of types is open and determined at runtime (plugins, user input)
- You need heterogeneous containers (e.g.,vector>)
- Binary size matters more than speed (templates generate more code)
- You want stable ABI across shared library boundaries
- You need to add new types without recompiling existing code

Use type erasure when:
- You want the interface simplicity of virtual dispatch but don't want to force users to inherit from your base class
- You need value semantics with runtime polymorphism (std::function,std::any)

Παγίδα

Ο μεταπρογραμματισμός προτύπων είναι ισχυρός αλλά έχει πραγματικό κόστος:

1. Έκρηξη χρόνου μεταγλώττισης — οι εμφωλευμένες σε βάθος στιγμιότυπα προτύπων μπορεί να κάνουν τους χρόνους μεταγλώττισης μη αποδεκτούς. Το αναδρομικό TMP είναι ιδιαίτερα κακό — προτιμήστε τις εκφράσεις fold και τοif constexpr.
2. Μηνύματα σφάλματος — χωρίς έννοιες, τα σφάλματα προτύπου παράγουν σελίδες με ακατανόητο κείμενο. Πάντα να περιορίζετε τα πρότυπα.
3. Εντοπισμός σφαλμάτων — είναι επώδυνη η μετάβαση στον κώδικα με πολλά πρότυπα σε ένα πρόγραμμα εντοπισμού σφαλμάτων. Τα περιτυλίγματα με διαγραφή τύπου προσθέτουν έμμεσες κατευθύνσεις που κρύβουν τον πραγματικό τύπο.
4. Αναγνωσιμότητα — Το CRTP, η σχεδίαση βάσει πολιτικής και τα πρότυπα έκφρασης είναι δύσκολο να κατανοήσουν οι μικροί προγραμματιστές. Τεκμηριώστε το όνομα και την πρόθεση του μοτίβου.
5. Υπερενδυνάμωση — δεν χρειάζεται κάθε πρόβλημα μια πρότυπη λύση. Μια απλή εικονική συνάρτηση ή ακόμα και μια δήλωση διακόπτη μπορεί να είναι πιο σαφής και επαρκής.

Βασικά συμπεράσματα
  • Το CRTP επιτρέπει τον στατικό πολυμορφισμό και τους μετρητές/μίξεις ανά κατηγορία προερχόμενοι από τοBase
  • Ο σχεδιασμός βάσει πολιτικής αποσυνθέτει τη συμπεριφορά σε παραμέτρους προτύπου — οι χρήστες συνθέτουν τον ακριβή συνδυασμό που χρειάζονται
  • Η διαγραφή τύπου συνδυάζει την ευελιξία προτύπου με τον πολυμορφισμό χρόνου εκτέλεσης (π.χ.std::function,std::any)
  • Τα πρότυπα έκφρασης εξαλείφουν τους προσωρινούς σε αλυσιδωμένες λειτουργίες — που χρησιμοποιούνται από τις βιβλιοθήκες Eigen, Blaze και παρόμοιες
  • Επιλέξτε πρότυπα για γνωστούς τύπους και μηδενικά γενικά έξοδα. εικονική αποστολή για σύνολα ανοιχτού τύπου και ευελιξία χρόνου εκτέλεσης
  • Αποφύγετε την υπερμηχανική — χρησιμοποιήστε τον απλούστερο μηχανισμό που ανταποκρίνεται στις απαιτήσεις σας

Κουίζ — Δοκιμάστε τις γνώσεις σας

(20 XP)

1. Στο CRTP, τι επιτυγχάνει το`class Dog : public Counter<Dog>`;

2. Τι πρόβλημα λύνει η διαγραφή τύπου;

3. Πότε πρέπει να προτιμάτε τις εικονικές λειτουργίες από τα πρότυπα;