رفتن به محتوا

الگوهای متابرنامه‌نویسی قالب

الگوهای پیشرفته قالب را تسلط یابید: CRTP برای چندریختی ایستا، طراحی مبتنی بر سیاست برای ترکیب منعطف، محو نوع برای عمومی‌سازی زمان اجرا، و راهنمایی برای زمان استفاده از قالب‌ها در مقابل ارسال مجازی.

فراتر از قالب‌های پایه

قالب‌ها الگوهای طراحی را ممکن می‌کنند که معادلی در زبان‌های دیگر ندارند. این الگوها از ماهیت زمان کامپایل قالب‌ها بهره می‌برند تا چندریختی ایستا (بدون سربار تابع مجازی)، رفتار ترکیب‌پذیر (سیاست‌ها) و محو نوع ایمن (پنهان کردن انواع مشخص پشت رابط یکنواخت) به دست آورند.

این‌ها الگوهای مبتدی نیستند — ابزارهایی برای نویسندگان کتابخانه و طراحان چارچوب هستند. درک آن‌ها برای خواندن درونیات کتابخانه استاندارد، Boost و چارچوب‌های مدرن ++C ضروری است.

CRTP: الگوی قالب بازگشتی عجیب (Curiously Recurring Template Pattern)

در 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
}

طراحی مبتنی بر سیاست (Policy-Based 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;
}

محو نوع (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) — مرور اجمالی

قالب‌های عبارت تکنیکی است که در آن عبارات حسابی روی اشیاء به‌جای محاسبه نتایج میانی، یک درخت زمان کامپایل از عملیات می‌سازند. نتیجه نهایی در یک گذر واحد محاسبه شده و اشیاء موقتی حذف می‌شوند.

به‌عنوان مثال، با یک کلاس Vector ساده، a + b + c دو بردار موقتی ایجاد می‌کند. با قالب‌های عبارت، a + b + c یک شیء عبارت سبک Add, Vector> می‌سازد که عناصر را بنا به تقاضا محاسبه می‌کند — بدون نیاز به موقتی‌ها.

این تکنیک توسط کتابخانه‌های جبر خطی با کارایی بالا مانند Eigen و Blaze استفاده می‌شود. درخت عبارت به‌صورت تنبل ارزیابی می‌شود و کامپایلر اغلب می‌تواند حلقه نتیجه تک‌گذره را برداری‌سازی کند.

قالب‌های عبارت پیاده‌سازی صحیح‌شان پیچیده است و عمدتاً نگرانی نویسنده کتابخانه هستند. اگر به ریاضیات ماتریس/بردار نیاز دارید، از کتابخانه‌ای مانند Eigen استفاده کنید به‌جای پیاده‌سازی قالب‌های عبارت خودتان.

بهترین شیوه

هنگام انتخاب بین قالب‌ها (چندریختی ایستا) و توابع مجازی (چندریختی پویا):

از قالب‌ها استفاده کنید وقتی:
- مجموعه انواع در زمان کامپایل مشخص است
- به انتزاع‌های بدون سربار نیاز دارید (بدون vtable، بدون واسطه‌گری)
- می‌خواهید کامپایلر بین مرزهای نوع درون‌خطی و بهینه‌سازی کند
- نیاز به کار با معناشناسی مقداری دارید (بدون تخصیص heap)
- عملکرد حیاتی است (حلقه‌های فشرده، کد عددی)

از توابع مجازی استفاده کنید وقتی:
- مجموعه انواع باز است و در زمان اجرا تعیین می‌شود (افزونه‌ها، ورودی کاربر)
- به ظروف ناهمگن نیاز دارید (مثلاً vector>)
- اندازه باینری بیش از سرعت اهمیت دارد (قالب‌ها کد بیشتری تولید می‌کنند)
- می‌خواهید ABI پایدار در مرزهای کتابخانه اشتراکی داشته باشید
- نیاز دارید انواع جدید بدون کامپایل مجدد کد موجود اضافه کنید

از محو نوع استفاده کنید وقتی:
- سادگی رابط ارسال مجازی را می‌خواهید اما نمی‌خواهید کاربران را مجبور به ارث‌بری از کلاس پایه خود کنید
- به معناشناسی مقداری با چندریختی زمان اجرا نیاز دارید (std::function، std::any)

دام

متابرنامه‌نویسی قالب قدرتمند است اما هزینه‌های واقعی دارد:

1. انفجار زمان کامپایل — نمونه‌سازی‌های قالب عمیقاً تودرتو می‌توانند زمان کامپایل را غیرقابل‌قبول کنند. TMP بازگشتی به‌ویژه بد است — عبارات تا و if constexpr را ترجیح دهید.
2. پیام‌های خطا — بدون مفاهیم، خطاهای قالب صفحات متن غیرقابل‌فهم تولید می‌کنند. همیشه قالب‌ها را محدود کنید.
3. اشکال‌زدایی — مرور گام‌به‌گام کد سنگین قالب در اشکال‌زدا دردناک است. پوشش‌های محو نوع واسطه‌گری اضافه می‌کنند که نوع واقعی را پنهان می‌کند.
4. خوانایی — CRTP، طراحی مبتنی بر سیاست و قالب‌های عبارت برای توسعه‌دهندگان تازه‌کار سخت‌فهم هستند. نام و هدف الگو را مستند کنید.
5. مهندسی بیش از حد — هر مشکلی نیاز به راه‌حل قالبی ندارد. یک تابع مجازی ساده یا حتی دستور switch ممکن است واضح‌تر و کافی باشد.

نکات کلیدی
  • CRTP چندریختی ایستا و شمارنده‌ها/mixin‌های هر کلاس را با مشتق شدن از Base ممکن می‌کند
  • طراحی مبتنی بر سیاست رفتار را به پارامترهای قالب تجزیه می‌کند — کاربران ترکیب دقیق مورد نیاز خود را می‌سازند
  • محو نوع انعطاف قالب را با چندریختی زمان اجرا ترکیب می‌کند (مثلاً std::function، std::any)
  • قالب‌های عبارت موقتی‌ها را در عملیات زنجیره‌ای حذف می‌کنند — توسط Eigen، Blaze و کتابخانه‌های مشابه استفاده می‌شوند
  • برای انواع شناخته‌شده و سربار صفر قالب‌ها را انتخاب کنید؛ برای مجموعه‌های نوع باز و انعطاف زمان اجرا ارسال مجازی را
  • از مهندسی بیش از حد اجتناب کنید — از ساده‌ترین مکانیزمی که الزامات شما را برآورده می‌کند استفاده کنید

آزمون — دانش خود را بسنجید

(20 امتیاز)

1. در CRTP، عبارت `class Dog : public Counter<Dog>` چه کاری انجام می‌دهد؟

2. محو نوع چه مشکلی را حل می‌کند؟

3. چه زمانی باید توابع مجازی را بر قالب‌ها ترجیح دهید؟