انتقل إلى المحتوى

تخصص القالب وإرسال العلامات

إتقان التخصص الكامل والجزئي، وتعلم متى تفضل التحميل الزائد، واكتشف إرسال العلامات و if constexpr كبدائل للتفرع في وقت الترجمة.

لماذا التخصص؟

يوفر القالب الأساسي تطبيقًا عامًا يعمل مع معظم الأنواع. لكن في بعض الأحيان يحتاج نوع معين إلى علاج مختلف جذريًا. على سبيل المثال،std::vectorهو تخصص يقوم بتجميع المنطقيات في وحدات بت من أجل كفاءة المساحة (على الرغم من أنه يعتبر خطأ على نطاق واسع في المكتبة القياسية).

تخصص القالب يتيح لك توفير تعريف بديل لوسيطات قالب محددة. هناك نوعان:

1. التخصص الكامل (الصريح) — يمكنك إصلاح جميع معلمات القالب لأنواع محددة.
2. التخصص الجزئي — يمكنك إصلاح بعض المعلمات أو تقييدها (على سبيل المثال، التخصص في جميع أنواع المؤشرات). التخصص الجزئي متاح فقط لقوالب الفصول الدراسية، وليس قوالب الوظائف.

التخصص كامل

يوفر التخصص الكامل تطبيقًا مخصصًا تمامًا لمجموعة واحدة محددة من وسائط القالب. يستخدم بناء الجملةtemplate <>(قائمة معلمات فارغة) متبوعة بالفئة أو الوظيفة ذات الوسائط المحددة.

full_specialization.cpp
#include <iostream>
#include <cstring>

// Primary template
template <typename T>
class Formatter {
public:
    static std::string format(const T& value) {
        return std::to_string(value);
    }
};

// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
    static std::string format(const char* value) {
        return std::string("\"" ) + value + "\"";
    }
};

// Full specialization for bool
template <>
class Formatter<bool> {
public:
    static std::string format(bool value) {
        return value ? "true" : "false";
    }
};

int main() {
    std::cout << Formatter<int>::format(42)            << '\n'; // 42
    std::cout << Formatter<const char*>::format("hi")  << '\n'; // "hi"
    std::cout << Formatter<bool>::format(true)          << '\n'; // true
}

التخصص الجزئي

التخصص الجزئي يقيد بعض معلمات القالب بينما يترك البعض الآخر عامًا. يعد هذا أمرًا قويًا للغاية لمطابقة عائلات الأنواع - جميع المؤشرات، وجميع المصفوفات، وجميع الأزواج، وما إلى ذلك. فقط قوالب الفئات/البنية/المتغيرة تدعم التخصص الجزئي.

partial_specialization.cpp
#include <iostream>
#include <string>
#include <typeinfo>

// Primary template
template <typename T>
struct TypeInfo {
    static std::string name() { return "general type"; }
};

// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
    static std::string name() {
        return "pointer to " + TypeInfo<T>::name();
    }
};

// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
    static std::string name() {
        return "reference to " + TypeInfo<T>::name();
    }
};

// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
    static std::string name() {
        return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
    }
};

int main() {
    std::cout << TypeInfo<int>::name()     << '\n'; // general type
    std::cout << TypeInfo<int*>::name()    << '\n'; // pointer to general type
    std::cout << TypeInfo<int**>::name()   << '\n'; // pointer to pointer to general type
    std::cout << TypeInfo<int[5]>::name()  << '\n'; // array of 5 general type
}
خطأ شائع

قوالب الوظائف لا يمكن أن تكون متخصصة جزئيًا — فقط متخصصة بالكامل. والأسوأ من ذلك أن تخصصات قوالب الوظائف لا تشارك في حل التحميل الزائد بالطريقة التي قد تتوقعها. يقوم المترجم أولاً بتحديد أفضل القالب الأساسي من خلال تحليل التحميل الزائد، وبعد ذلك فقط يتحقق مما إذا كان هذا القالب الأساسي يحتوي على تخصص مطابق.

وهذا يؤدي إلى سلوك مفاجئ:

template <typename T> void f(T)   { /* #1 - primary */ }
template <typename T> void f(T*)  { /* #2 - overload for pointers */ }
template <> void f<int*>(int*)     { /* #3 - specialization of #1 */ }

Calling f((int*)nullptr) selects #2 (the better overload match), not #3 (which specializes #1, not #2). The fix: use regular function overloading instead of function template specialization. If you need compile-time type branching, use if constexpr أو المفاهيم.

نمط إرسال العلامة

يستخدم إرسال العلامات أنواع البنية الفارغة ("العلامات") لتحديد الأحمال الزائدة في وقت الترجمة. تستخدم المكتبة القياسية هذا على نطاق واسع - يرسلstd::advanceعلى علامات فئة المكرر. كانت هذه هي تقنية التفرع في وقت الترجمة السائدة قبلif constexpr.

tag_dispatch.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

namespace detail {
    // Tags are empty structs used purely for overload selection
    template <typename Iter>
    void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
        it += n;  // O(1) — random access
        std::cout << "random access advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
        // O(n) — step one at a time, forward or backward
        while (n > 0) { ++it; --n; }
        while (n < 0) { --it; ++n; }
        std::cout << "bidirectional advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::input_iterator_tag) {
        while (n > 0) { ++it; --n; }
        std::cout << "input iterator advance\n";
    }
}

template <typename Iter>
void my_advance(Iter& it, int n) {
    // iterator_traits extracts the category tag
    detail::advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    auto vit = v.begin();
    my_advance(vit, 3);  // random access advance

    std::list<int> l{1, 2, 3, 4, 5};
    auto lit = l.begin();
    my_advance(lit, 3);  // bidirectional advance
}

إذا كان constexpr: البديل الحديث

يوفر if constexpr الخاص بـ C++17 طريقة أنظف للتفرع في وقت الترجمة ضمن وظيفة واحدة. لم يتم إنشاء مثيل للفرع المهمل، لذلك لا يحتاج إلى التحويل البرمجي للنوع المحدد. يحل هذا إلى حد كبير محل إرسال العلامة وSFINAE للحالات البسيطة.

if_constexpr.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

template <typename T>
std::string stringify(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_same_v<T, std::string>) {
        return '"' + value + '"';
    } else if constexpr (std::is_same_v<T, const char*>) {
        return std::string("\"" ) + value + "\"";
    } else {
        return "[non-printable]";
    }
}

int main() {
    std::cout << stringify(42)              << '\n'; // 42
    std::cout << stringify(3.14)            << '\n'; // 3.140000
    std::cout << stringify(std::string("hello")) << '\n'; // "hello"
    std::cout << stringify("world")         << '\n'; // "world"
}
أفضل ممارسة

استخدم دليل القرار هذا للتفرع في وقت الترجمة:

- if constexpr(C++17) — الأبسط، ويعمل ضمن وظيفة واحدة، ولا حاجة إلى أنماط خاصة. تفضل هذا للمنطق المباشر القائم على النوع.
- المفاهيم/المتطلبات (C++20) — الأفضل لتقييد التحميل الزائد للوظائف أو القوالب بأكملها. التوثيق الذاتي ويعطي رسائل خطأ واضحة.
- إرسال العلامات — لا يزال مفيدًا عندما يكون لديك تسلسل هرمي للعلامات موجود (مثل فئات التكرار). تعتمد قواعد التعليمات البرمجية لما قبل C++17 بشكل كبير على هذا.
- SFINAE /enable_if — أقدم تقنية. من الصعب قراءة الأخطاء المبهمة. تجنب استخدام التعليمات البرمجية الجديدة إذا كانت المفاهيم أوif constexprتعمل.
- التخصص الكامل — يُستخدم لقوالب الفصل الدراسي عندما تحتاج إلى تنفيذ مختلف تمامًا لنوع واحد محدد.
- لا تستخدم مطلقًا تخصص قالب الوظيفة — استخدم التحميل الزائد بدلاً من ذلك.

النقاط الرئيسية
  • يوفر التخصص الكامل (template <>) تطبيقًا مخصصًا لنوع واحد محدد
  • يقيد التخصص الجزئي بعض المعلمات - وهو متاح لقوالب الفصل الدراسي فقط، وليس لقوالب الوظائف
  • يعد تخصص قالب الوظيفة عرضة للأخطاء - تفضل التحميل الزائد أوif constexpr
  • يستخدم إرسال العلامات أنواع بنية فارغة لتحديد التحميل الزائد في وقت الترجمة عبرiterator_traits
  • if constexpr(C++17) هو البديل الحديث والأنظف الذي يتخلص من الفروع غير المأخوذة في وقت الترجمة

اختبار — اختبر معرفتك

(20 نقطة خبرة)

1. لماذا يجب عليك تجنب قوالب الوظائف المتخصصة؟

2. ما هي أنواع القوالب التي تدعم التخصص الجزئي؟

3. ما الذي يفعله`if constexpr`بشكل مختلف عن`if`العادي؟