أنماط البرمجة الفوقية للقالب
إتقان أنماط القوالب المتقدمة: CRTP لتعدد الأشكال الثابت، والتصميم القائم على السياسة للتكوين المرن، ومحو الكتابة للأدوية العامة في وقت التشغيل، وإرشادات حول متى يتم استخدام القوالب مقابل الإرسال الافتراضي.
ما وراء القوالب الأساسية
تعمل القوالب على تمكين أنماط التصميم التي لا يوجد لها مثيل في اللغات الأخرى. تستغل هذه الأنماط طبيعة وقت الترجمة للقوالب لتحقيق تعدد الأشكال الثابت (بدون تحميل وظيفة افتراضية)، والسلوك القابل للتركيب (السياسات)، والمحو الآمن للنوع (إخفاء أنواع محددة خلف واجهة موحدة).
هذه ليست أنماطًا للمبتدئين، بل هي أدوات لمؤلفي المكتبات ومصممي إطار العمل. يعد فهمها أمرًا ضروريًا لقراءة الأجزاء الداخلية القياسية للمكتبة، وBoost، وأطر عمل C++ الحديثة.
CRTP: نمط القالب المتكرر الغريب
في CRTP، يتم اشتقاق الفصل من قالب تم إنشاء مثيل له بـ نفسه كوسيطة:class Derived : public Base. يمكن للفئة الأساسية استدعاء الأساليب الموجودة في الفئة المشتقة في وقت الترجمة — بدون أي حمل افتراضي للإرسال. يُستخدم هذا لإدخال السلوك وتنفيذ تعدد الأشكال الثابت وإنشاء وظيفة mixin.
#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
}التصميم القائم على السياسات
التصميم القائم على السياسات يقسم الفصل إلى مكونات سلوك قابلة للتبديل (سياسات) يتم تمريرها كمعلمات قالب. تنفذ كل سياسة جانبًا واحدًا من السلوك. يقوم المستخدمون بتكوين المجموعة الدقيقة التي يحتاجونها. تم نشر هذا من خلال "تصميم C++ الحديث" لأندريه ألكسندريسكو.
#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;
}اكتب المحو
محو الكتابة يخفي النوع الملموس خلف واجهة موحدة، ويجمع بين مرونة القوالب وتعدد أشكال الوظائف الافتراضية في وقت التشغيل. المثال الكلاسيكي هوstd::function، والذي يمكنه الاحتفاظ بأي شيء قابل للاستدعاء. يستخدم النمط ثلاثة مكونات: المفهوم (الواجهة المجردة)، والنموذج (غلاف القالب الذي ينفذ الواجهة لأي نوع)، وفئة المقبض الخارجي.
#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
}قوالب التعبير (نظرة عامة)
قوالب التعبير هي تقنية حيث تقوم التعبيرات الحسابية على الكائنات ببناء شجرة وقت الترجمة للعمليات بدلاً من حساب النتائج المتوسطة. يتم حساب النتيجة النهائية في مسار واحد، مما يؤدي إلى إزالة الكائنات المؤقتة.
على سبيل المثال، مع فئةVectorالساذجة، يقومa + b + cبإنشاء متجهين مؤقتين. باستخدام قوالب التعبير، يقومa + b + cبإنشاء كائن تعبير خفيف الوزنAddيحسب العناصر عند الطلب - دون الحاجة إلى مؤقتات.
يتم استخدام هذه التقنية من خلال مكتبات الجبر الخطي عالية الأداء مثل Eigen و Blaze. يتم تقييم شجرة التعبير بتكاسل، ويمكن للمترجم غالبًا توجيه حلقة المرور المفردة الناتجة.
تعتبر قوالب التعبير معقدة بحيث يصعب تنفيذها بشكل صحيح وهي في المقام الأول من اهتمامات مؤلفي المكتبة. إذا كنت بحاجة إلى رياضيات المصفوفات/المتجهات، فاستخدم مكتبة مثل Eigen بدلاً من تنفيذ قوالب التعبير بنفسك.
عند الاختيار بين القوالب (تعدد الأشكال الثابت) والوظائف الافتراضية (تعدد الأشكال الديناميكي):
استخدم النماذج عندما:
- مجموعة الأنواع معروفة في وقت الترجمة
- أنت بحاجة إلى تجريدات صفرية (لا يوجد جدول جدولي، ولا يوجد اتجاه غير مباشر)
- تريد أن يقوم المترجم بتضمين وتحسين حدود الكتابة
- أنت بحاجة إلى العمل مع دلالات القيمة (بدون تخصيص الكومة)
- الأداء أمر بالغ الأهمية (حلقات ضيقة، رمز رقمي)
استخدم الوظائف الافتراضية عندما:
- مجموعة الأنواع مفتوحة ويتم تحديدها في وقت التشغيل (المكونات الإضافية، إدخال المستخدم)
- أنت بحاجة إلى حاويات غير متجانسة (على سبيل المثال،vector)
- الحجم الثنائي مهم أكثر من السرعة (تولد القوالب المزيد من التعليمات البرمجية)
- تريد ABI مستقرًا عبر حدود المكتبة المشتركة
- تحتاج إلى إضافة أنواع جديدة دون إعادة ترجمة التعليمات البرمجية الموجودة
استخدم مسح الكتابة عندما:
- أنت تريد بساطة واجهة الإرسال الافتراضي ولكنك لا ترغب في إجبار المستخدمين على الوراثة من فئتك الأساسية
- أنت بحاجة إلى دلالات القيمة مع تعدد أشكال وقت التشغيل (std::function،std::any)
تعد البرمجة الوصفية للقالب قوية ولكن لها تكاليف حقيقية:
1. انفجار وقت الترجمة — يمكن أن تؤدي عمليات إنشاء النماذج المتداخلة بشدة إلى جعل أوقات الترجمة غير مقبولة. يعد TMP العودي سيئًا بشكل خاص - تفضل تعبيرات الطية وif constexpr.
2. رسائل الخطأ — بدون مفاهيم، تنتج أخطاء القالب صفحات تحتوي على نص غير مفهوم. قم دائمًا بتقييد القوالب.
3. تصحيح الأخطاء — يعد التنقل عبر التعليمات البرمجية ذات القوالب الثقيلة في مصحح الأخطاء أمرًا مؤلمًا. تضيف الأغلفة التي تم مسحها نوعاً غير مباشر يخفي النوع الفعلي.
4. قابلية القراءة — يصعب على المطورين المبتدئين فهم CRTP والتصميم القائم على السياسة وقوالب التعبير. قم بتوثيق اسم النمط والقصد منه.
5. الهندسة المفرطة — ليست كل مشكلة تحتاج إلى حل نموذجي. قد تكون الوظيفة الافتراضية البسيطة أو حتى بيان التبديل أكثر وضوحًا وكفاية.
- يتيح CRTP تعدد الأشكال الثابت والعدادات/الخلطات لكل فئة من خلال الاشتقاق من
Base - يقوم التصميم القائم على السياسة بتحليل السلوك إلى معلمات القالب - حيث يقوم المستخدمون بتكوين المجموعة الدقيقة التي يحتاجون إليها
- يجمع مسح الكتابة بين مرونة القالب وتعدد الأشكال في وقت التشغيل (على سبيل المثال،
std::function،std::any) - تعمل قوالب التعبير على إزالة العناصر المؤقتة في العمليات المتسلسلة - التي تستخدمها Eigen وBlaze والمكتبات المشابهة
- اختر قوالب للأنواع المعروفة بدون أي تكاليف إضافية؛ إرسال افتراضي لمجموعات الكتابة المفتوحة ومرونة وقت التشغيل
- تجنب الإفراط في الهندسة – استخدم أبسط آلية تلبي متطلباتك
اختبار — اختبر معرفتك
(20 نقطة خبرة)1. في CRTP، ما الذي يحققه`class Dog : public Counter<Dog>`؟
2. ما هي المشكلة التي يحلها مسح الكتابة؟
3. متى يجب أن تفضل الوظائف الافتراضية على القوالب؟