اذهب إلى المحتوى

القوالب Templates في Cpp


محمد بغات

صار بالإمكان قولبة الأصناف والدوالّ والمتغيّرات في لغة ++C منذ C++‎ 14، والقالب هو شيفرة لها بعض المعاملات الحرّة (free parameters) التي ستُستبدَل فيما بعد بأصناف أو دوال أو متغيّرات حقيقية عند تحديد تلك المعاملات.

وقد تكون المعاملات أنواعًا أو قيمًا أو قوالب بحد ذاتها، ومن أمثلة القوالب: المتجهات (‎std::vector‎)، التي تصبح أنواع حاويات حقيقية عند تحديد نوع العنصر، كما في ‎std::vector<int>‎.

قالب صنف أولي Basic Class Template

الفكرة الأساسية لقالب الصنف هي أنّ مُعامل القالب سيُستبدَل بنوع معيّن في وقت التصريف، ونتيجة لذلك يمكن استخدام نفس الصنف مع عدّة أنواع. يحدّد المستخدمُ النوعَ الذي سيُستخدَم عند التصريح عن متغيّر من ذلك الصنف، ولدينا ثلاثة أمثلة على ذلك:

#include <iostream>
using std::cout;
template < typename T >            // صنف بسيط لاحتواء عدد من أيّ نوع
    class Number
    {
        public:
            void setNum(T n);            // ضبط حقل الصنف عند العدد المُعطى
        T plus1() const;            // "follower" يعيد حقل الصنف
        private:
            T num;                // حقل من الصنف
    };
template < typename T >            // ضبط حقل الصنف عند العدد المُعطى
    void Number<T>::setNum(T n)
    {
        num = n;
    }

template < typename T >            // "follower" إعادة
    T Number<T>::plus1() const
    {
        return num + 1;
    }

int main()
{
    Number<int> anInt;            // (في الصنف T ستستبدل int) إجراء اختبار مع عدد صحيح 
    anInt.setNum(1);
    cout << "My integer + 1 is " << anInt.plus1() << "\n";        // 2 يطبع

  Number<double> aDouble;        // double الاختبار بعدد من النوع
    aDouble.setNum(3.1415926535897);
    cout << "My double + 1 is " << aDouble.plus1() << "\n";    // يطبع 4.14159

    Number<float> aFloat;            // الاختبار بعدد عشري
    aFloat.setNum(1.4);
    cout << "My float + 1 is " << aFloat.plus1() << "\n";        // يطبع 2.4
    return 0;    
}

قوالب الدوال Function Templates

يمكن تطبيق القولبة على الدوالّ (والهياكل التقليدية الأخرى)، انظر المثال التالي حيث تمثل T نوعًا مجهولًا، ويكون كلا الوسيطين من نفس النوع:

template < typename T>
    void printSum(T add1, T add2)
    {
        std::cout << (add1 + add2) << std::endl;
    }

يمكن استخدامها بنفس الطريقة التي تستخدم بها قوالب الهياكل (structure templates).

printSum<int> (4, 5);
printSum<float> (4.5 f, 8.9 f);

يُستخدَم وسيط القالب في كلتا الحالتين لاستبدال أنواع المعاملات؛ وستعمل النتيجة بشكل مشابه لدوالّ C++‎ العادية، فإذا لم تتطابق المعاملات مع نوع القالب، فإنّ المٌصرّف سيطبّق التحويلات القياسية.

إحدى الخصائص الإضافية لدوالّ القوالب -على عكس أصناف القوالب- هي أنّ المُصرّف يمكنه استنتاج معاملات القالب بناءً على المعاملات المُمرّرة إلى الدالّة.

في الحالة التالية يكون كلا المعاملين من نوع int، وذلك يتيح للمصرف استنباط النوع، وتكون T مساوية لـ int:

printSum(4, 5);    

في هذه الحالة يكون المعاملان من نوعين مختلفين، ويعجز المصرف عن استنتاج نوع T بسبب وجود تناقضات، ونتيجة لهذا يحدث خطأ في التصريف.

printSum(5.0, 4);

تتيح لنا هذه الميزة تبسيط الشيفرة عند الجمع بين بنيات ودوالّ القالب. يوجد نمط شائع في المكتبة القياسية يسمح لنا بجعل ‎template structure X‎ تستخدم دالّة مساعدة ‎make_X()‎. انظر المثال التالي الذي يوضح كيف يبدو نمط make_X:

  1. هيكل قالب مع نوع قالب واحد أو أكثر:
template < typename T1, typename T2>
    struct MyPair
    {
        T1 first;
        T2 second;
    };
  1. دالة make لها نوع لكل معاملات القوالب في هيكل القالب
template < typename T1, typename T2>
    MyPair<T1, T2> make_MyPair(T1 t1, T2 t2)
    {
        return MyPair<T1, T2> { t1, t2 };
    }

كيف يساعد هذا؟ انظر ما يلي حيث يكون val1 و val2 من نفس النوع:

auto val1 = MyPair<int, float> { 5, 8.7 };   // إنشاء الكائن يعرّف الأنواع صراحة
auto val2 = make_MyPair(5, 8.7);        // إنشاء الكائن باستخدام أنواع المعاملات

ملاحظة: لم يُصمّم هذا لاختصار الشيفرة وإنّما صُمِّم لجعلها أكثر متانة إذ يسمح بتغيير الأنواع عن طريق تغيير الشيفرَة في مكان واحد وليس في مواقع متعدّدة.

قوالب التعبير Expression templates

قوالب التعابير هي تقنية تحسين (optimization) في وقت التصريف تُستخدم في الغالب في الحوسبة العلمية (scientific computing)، والغرض الرئيسي منها هو تجنّب إهدار الوقت وتحسين الحسابات باستخدام مسار واحد -عادةً عند إجراء عمليات على المجاميع العددية-.

في البداية، تُقسَّم قوالب التعابير من أجل التحايل على الفوارق الناجمة عن التحميل الزائد عند تنفيذ أنواع المصفوفات (‎Array‎) أو المصفوفات المتعددة (‎Matrix‎).

ويجب أن تفهم الهدف من قوالب التعبير أولًا قبل الغوص فيها، انظر الصنف Matrix الوارد في المثال أدناه:

template < typename T, std::size_t COL, std::size_t ROW>
    class Matrix
    {
        public:
            using value_type = T;
        Matrix(): values(COL *ROW) {}

        static size_t cols()
        {
            return COL;
        }

        static size_t rows()
        {
            return ROW;
        }

        const T &operator()(size_t x, size_t y) const
        {
            return values[y *COL + x];
        }

        T &operator()(size_t x, size_t y)
        {
            return values[y *COL + x];
        }

        private:
            std::vector<T> values;
    };
template < typename T, std::size_t COL, std::size_t ROW>
    Matrix<T, COL, ROW>
    operator+(const Matrix<T, COL, ROW> &lhs, const Matrix<T, COL, ROW> &rhs)
    {
        Matrix<T, COL, ROW> result;
        for (size_t y = 0; y != lhs.rows(); ++y)
        {
            for (size_t x = 0; x != lhs.cols(); ++x)
            {
                result(x, y) = lhs(x, y) + rhs(x, y);
            }
        }

        return result;
    }

يمكنك الآن كتابة تعبيرات Matrix على النحو:

const std::size_t cols = 2000;
const std::size_t rows = 1000;
Matrix<double, cols, rows> a, b, c;
// a, b &c هيئ
for (std::size_t y = 0; y != rows; ++y)
{
    for (std::size_t x = 0; x != cols; ++x)
    {
        a(x, y) = 1.0;
        b(x, y) = 2.0;
        c(x, y) = 3.0;
    }
}

Matrix<double, cols, rows> d = a + b + c;    // d(x, y) = 6

كما هو مُوضّح أعلاه، فتستطيع توفير صيغة تحاكي الصيغة الرياضية المعتادة للمصفوفات عبر التحميل الزائد للعامل ‎operator+()‎، لكن التنفيذ السابق غير فعّال مقارنةً بالإصدارات المكافئة "المصنوعة يدويًا".

ولفهم السبب، عليك مراعاة ما يحدث عند كتابة تعبير مثل ‎Matrix d = a + b‎ + c‎، يُنشر هذا التعبير في الواقع إلى التعبير ‎((a + b) + c)‎، أو ‎operator+(operator+(a, b), c)‎، أي تُنفّذ الحلقة الموجودة داخل operator+()‎ مرّتين، بينما كان من الممكن إجراؤها مرّة واحدة. هذا يؤدّي أيضًا إلى إنشاء كائنين مؤقّتين، مما يضعف الأداء أكثر. ويبدو أنّ المرونة التي حصلنا عليها والناتجة عن استخدام صياغة قريبة للصياغة المعمول بها في الرياضيات كانت على حساب الأداء.

على سبيل المثال، تستطيع تنفيذ تجميع مصفوفةٍ بأسلوب أفضل بدون التحميل الزائد للعامل، وباستخدام مسار تمرير واحد:

template < typename T, std::size_t COL, std::size_t ROW>
    Matrix<T, COL, ROW> add3(const Matrix<T, COL, ROW> &a, const Matrix<T, COL, ROW> &b, const Matrix<T, COL, ROW> &c)
    {
        Matrix<T, COL, ROW> result;
        for (size_t y = 0; y != ROW; ++y)
        {
            for (size_t x = 0; x != COL; ++x)
            {
                result(x, y) = a(x, y) + b(x, y) + c(x, y);
            }
        }

        return result;
    }

لكنّ المثال السابق له عيوبه، لأنّه ينشئ واجهة أكثر تعقيدًا للصنف Matrix، إذ سيكون عليك اعتبار توابع من قبيل: ‎Matrix::add2()‎ و ‎Matrix::AddMultiply()‎. بدلاً من ذلك، دعنا نرجع خطوة إلى الوراء ونرى كيف يمكننا تكييف التحميل الزائد للمُعامل لأجل تحسين الأداء.

تنبع المشكلة من حقيقة أنّ التعبير ‎Matrix d = a + b + c‎ يُقيَّم قبل إنشاء شجرة التعبير بأكملها، أي ما نريده حقًا هو تقييم ‎a + b + c‎ مرّة واحدة، وفقط عندما تحتاج إلى إسناد التعبير الناتج إلى ‎d‎.

وتلك هي الفكرة الأساسية وراء قوالب التعبير: بدلاً من أن يُقيّم المعامل ‎operator+()‎ نتيجة إضافة نسختين من Matrix على الفور، فسيُعيد "قالبَ تعبير" لتقييمه مستقبلًا بعد الانتهاء من بناء شجرة التعبير بأكملها.

على سبيل المثال، فيما يلي تنفيذ ممكن لقالب تعبير يتوافق مع جمع عنصرين من نوعين مختلفين:

template < typename LHS, typename RHS>
    class MatrixSum
    {
        public:
            using value_type = typename LHS::value_type;
        MatrixSum(const LHS &lhs, const RHS &rhs): rhs(rhs), lhs(lhs) {}

        value_type operator()(int x, int y) const
        {
            return lhs(x, y) + rhs(x, y);
        }

        private:
            const LHS &lhs;
        const RHS &rhs;
    };

وهذه هي النسخة المُحدثة من ‎operator+()‎:

template < typename LHS, typename RHS>
    MatrixSum<LHS, RHS> operator+(const LHS &lhs, const LHS &rhs)
    {
        return MatrixSum<LHS, RHS> (lhs, rhs);
    }

كما ترى، لا يعيد ‎operator+()‎ "تقييمًا مُتسرّعًا" بعد الآن لنتيجَةِ إضافة نسختين من Matrix (والتي ستكون نسخة أخرى من Matrix)، ولكنه يعيد قالب تعبير يمثّل عملية الإضافة. ربما يجب أن تتذكّر أنّ التعبير لم يُقيَّم بعد، وإنما يخزّن مراجع إلى معاملاته وحسب. وفي الحقيقة، لا شيء يمنعك من إنشاء قالب تعبير ‎MatrixSum<>‎ كما يلي:

MatrixSum<Matrix < double>, Matrix<double>> SumAB(a, b);

يمكنك تقييم التعبير ‎d =‎‎a + ‎b‎ في مرحلة لاحقة، حين تحتاج فعليًا إلى نتيجة الجمع، كما يلي:

for (std::size_t y = 0; y != a.rows(); ++y)
{
    for (std::size_t x = 0; x != a.cols(); ++x)
    {
        d(x, y) = SumAB(x, y);
    }
}

كما ترى، هناك فائدة أخرى من استخدام قوالب التعبير وهي أنك ستتمكّن من تقييم مجموع ‎a‎ و ‎b‎ وإسناده إلى ‎d‎ مرّة واحد. أيضًا لا شيء يمنعك من الجمع بين عدّة قوالب تعبير، فقد يُنتج ‎a + b + c‎ قالب التعبير التالي:

MatrixSum<MatrixSum<Matrix<double>, Matrix<double> >, Matrix<double>> SumABC(SumAB, c);

وهنا، مرّة أخرى، يمكنك تقييم النتيجة النهائية مرّة واحدة:

for (std::size_t y = 0; y != a.rows(); ++y)
{
    for (std::size_t x = 0; x != a.cols(); ++x)
    {
        d(x, y) = SumABC(x, y);
    }
}

أخيرًا، آخر قطعة من اللغز هي توصيل قالب التعبير الخاص بك بالصنف ‎Matrix‎، من خلال تنفيذ العامل ‎Matrix::operator=()‎، الذي يأخذ قالب التعبير كوسيط ويقيِّمه في تمريرة واحدة كما فعلتَ "يدويًا" من قبل:

template < typename T, std::size_t COL, std::size_t ROW>
    class Matrix
    {
        public:
            using value_type = T;
        Matrix(): values(COL *ROW) {}

        static size_t cols()
        {
            return COL;
        }

        static size_t rows()
        {
            return ROW;
        }

        const T &operator()(size_t x, size_t y) const
        {
            return values[y *COL + x];
        }

        T &operator()(size_t x, size_t y)
        {
            return values[y *COL + x];
        }

        template < typename E>
            Matrix<T, COL, ROW> &operator=(const E &expression)
            {
                for (std::size_t y = 0; y != rows(); ++y)
                {
                    for (std::size_t x = 0; x != cols(); ++x)
                    {
                        values[y *COL + x] = expression(x, y);
                    }
                }

                return * this;
            }

        private:
            std::vector<T> values;
    };

هياكل بيانات قالب متغاير

الإصدار ≥ C++‎ 14

من المفيد أحيانًا تعريف أصناف أو بنيات متغايرة لا يُعرَّف عدد حقولها وأنواعها إلا في وقت التصريف، ويُعدُّ ‎std::tuple‎ أحد الأمثلة الأساسية على ذلك، لكن قد تحتاج أحيانًا إلى تعريف هياكلك المخصّصة. انظر المثال التالي الذي يعرّف بنية باستخدام التركيب (compounding) بدلاً من الوراثة كما هو الحال في ‎std::tuple‎.

سنبدأ بالتعريف العام (الفارغ)، والذي يمكن أن ينفع أيضًا كحالة أوّلية (base-case) لإنهاء التكرارية في التخصيص اللاحق:

template < typename...T >
    struct DataStructure {};

يسمح لنا هذا بتعريف بنية فارغة ‎DataStructure<> data‎، ولكنّ هذا غير مفيد حاليًا. بعد ذلك يأتي تخصيص الحالة التكرارية (recursive case specialisation):

template < typename T, typename...Rest >
    struct DataStructure<T, Rest... >
    {
        DataStructure(const T &first, const Rest &...rest): first(first), rest(rest...) {}

        T first;
        DataStructure < Rest... > rest;
    };

أصبح المثال مناسبًا الآن لإنشاء هياكل بيانات عشوائية، مثل <DataStructure<int, float, std::string‎، و("data(1, 2.1, "hello.

لاحظ أنّ هذا التخصيص يتطّلب وجود مُعامل قالب واحد على الأقل -وهو ‎T‎ أعلاه- دون إعارة اهتمام لخصوصيات الحُزمة ‎Rest‎، ويسمح إدراك وجود ‎T‎ بتعريف حقل ‎first‎. وتُحزَم بقية البيانات ذاتيًا على شكل ‎DataStructure‎ <‎Rest ...‎> ‎rest‎، ويهيّئ المنشئ كلا العُضوَين مع استدعاء مُنشئ ذاتي على العضو ‎rest‎.

لفهم هذا بشكل أفضل، إليك المثال التالي: لنفترض أنّ لديك تصريحًا <‎DataStructure<int,‎ ‎float‎. في البداية يتطابق التصريح مع التخصيص، وذلك يؤدّي إلى بنية تحتوي الحقلينint first و DataStructure<float> rest. ويتطابق التعريف ‎rest‎ مرّة أخرى مع التخصيص إذ يُنشئ حقلين float first و DataStructure<> rest خاصّين به. أخيرًا، يُطابَق rest مع الحالة الأساسية (base-case)، ممّا ينتج عنه بنية فارغة.

يمكنك تصور هذا على النحو التالي:

DataStructure<int, float>
    - > int first 
-> DataStructure<float> rest 
            -> float first 
           - > DataStructure < > rest 
                -> (empty)

أصبح لدينا الآن بنية بيانات، ولكنّها ليست مفيدة حاليًا، إذ لا يمكننا الوصول بسهولة إلى العناصر الفردية للبيانات. على سبيل المثال، سيتعيّن علينا استخدام ‎data.rest.rest.first‎ للوصول إلى العضو الأخير في ‎DataStructure<int, float, std::string> data‎، وذلك صعب، لذا سنضيف تابع ‎get‎ إليها -مطلوب فقط في التخصيص، لأنّ بنية الحالة الأساسية لا تحتوي على بيانات أصلًا-:

template < typename T, typename...Rest >
    struct DataStructure<T, Rest... >
    {
        ...
        template < size_t idx>
        auto get()
            {
                return GetHelper<idx, DataStructure<T, Rest... >>::get(*this);
            }

            ...
    };

كما ترى، فإنّ التابع ‎get‎ نفسه مُقولَب -هذه المرّة على فهرس العضو المطلوب (لذلك يمكن استخدامه على النحو ‎data.get<1>()‎، على غرار الصفوف ‎std::tuple‎-، ويتم العمل الفعلي بفضل دالّة ساكنة في الصنف المساعد ‎GetHelper‎.

السبب في أنّنا لم نتمكن من تعريف الدالّة المطلوبة مباشرة في التابع ‎get‎ الخاص بـ ‎DataStructure‎ هو أنّنا (كما سنرى قريبًا) نحتاج إلى تخصيص ‎idx‎، وهذا مستحيل لأنّه لا يمكن تخصيص تابع القالب دون تخصيص قالب الصنف الحاوي. لاحظ أيضًا أنّ استخدام ‎auto‎ من نمط C++14 بسّط عملنا كثيرًا، إذ بدونها سيكون تعبير نوع القيمة المُعادة معقدًا.

سنحتاج إلى تصريح مُسبق فارغ وتَخصيصَين في الصنف المساعد.

أولا التصريح:

template < size_t idx, typename T>
    struct GetHelper;

في الحالة الأساسية (‎idx==‎ ‎0‎)، سنعيد العضو ‎first‎ فقط:

template < typename T, typename...Rest >
    struct GetHelper<0, DataStructure<T, Rest... >>
    {
        static T get(DataStructure<T, Rest... > &data)
        {
            return data.first;
        }
    };

في حالة التكرارية، سنُنقِص قيمة ‎idx‎ ونستدعي ‎GetHelper‎على العضو ‎rest‎:

template < size_t idx, typename T, typename...Rest >
    struct GetHelper<idx, DataStructure<T, Rest... >>
    {
        static auto get(DataStructure<T, Rest... > &data)
        {
            return GetHelper < idx - 1, DataStructure < Rest... >>::get(data.rest);
        }
    };

لنفترض أنّ لدينا ‎DataStructure<int, float> data‎ ونحتاج إلى ‎data.get<1>()‎، هذا سيَستدعي ‎GetHelper<1, DataStructure<int, float>>::get(data)‎ (التخصيص الثاني)، والذي سيستدعي بدوره ‎‎GetHelper<0, DataStructure<float>>::get(data.rest)‎، والتي تُعيد في النهاية (بحسب التخصيص الأوّل، إذ أنّ ‎idx‎ تساوي الآن 0) ‎data.rest.first‎.

هذا كل شيء! في ما يلي الشيفرة الكاملة، مع مثال توضيحي في الدالة ‎main‎:

#include <iostream>

template < size_t idx, typename T>
    struct GetHelper;
template < typename...T >
    struct DataStructure {};
template < typename T, typename...Rest >
    struct DataStructure<T, Rest... >
    {
        DataStructure(const T &first, const Rest &...rest): first(first), rest(rest...) {}

        T first;
        DataStructure < Rest... > rest;

        template < size_t idx>
        auto get()
        {
            return GetHelper<idx, DataStructure<T, Rest... >>::get(*this);
        }
    };
template < typename T, typename...Rest >
    struct GetHelper<0, DataStructure<T, Rest... >>
    {
        static T get(DataStructure<T, Rest... > &data)
        {
            return data.first;
        }
    };
template < size_t idx, typename T, typename...Rest >
    struct GetHelper<idx, DataStructure<T, Rest... >>
    {
        static auto get(DataStructure<T, Rest... > &data)
        {
            return GetHelper < idx - 1, DataStructure < Rest... >>::get(data.rest);
        }
    };
int main()
{
    DataStructure<int, float, std::string > data(1, 2.1, "Hello");

    std::cout << data.get<0> () << std::endl;
    std::cout << data.get<1> () << std::endl;
    std::cout << data.get<2> () << std::endl;

    return 0;
}

إعادة توجيه الوسائط

يمكن أن يقبل القالبُ المراجعَ اليمينية (rvalue references) واليسارية (lvalue references) على السواء باستخدام مرجع إعادة توجيه (forwarding reference):

template < typename T>
    void f(T && t);

في هذه الحالة، سيُستنبَط النوع الحقيقي لـ ‎t‎ من السياق:

struct X {};
X x;
f(x);    // f<X&>(x) تستدعي
f(X());    // f < X>(x) تستدعي

في الحالة الأولى، يُستنتَج النوع ‎T‎ كمرجع إلى X ‏(‎X&‎)، أما نوع ‎t‎ فهو مرجع يساري إلى X، بينما في الحالة الثانية يُستنتَج نوع ‎T‎ كـ ‎X‎، ونوع ‎t‎ كمرجع يميني إلى X ‏(‎X&&‎).

ملاحظة: تجدر الإشارة إلى أنّه في الحالة الأولى، يكون ‎decltype(t)‎ و ‎T‎ متكافئان، وذلك على خلاف الحالة الثانية. ولأجل إعادة توجيه (forward)‏ ‎t‎ إلى دالّة أخرى بالشكل الصحيح، سواء كان مرجعًا يمينيا أو يساريا، فينبغي استخدام std::forward:

template < typename T>
    void f(T && t)
    {
        g(std::forward<T> (t));
    }

يمكن استخدام "إعادة توجيه المراجع" (Forwarding references) مع القوالب المتغايرة (variadic templates):

template < typename...Args >
    void f(Args && ...args)
    {
        g(std::forward<Args> (args)...);
    }

ملاحظة: لا يمكن استخدام إعادة توجيه المراجع إلا مع معاملات القوالب، مثلًا في الشيفرة التالية، ‎v‎ هي مرجع يميني، وليست مرجع إعادة توجيه:

#include <vector>

template < typename T>
    void f(std::vector<T> && v);

التخصيص الجزئي للقوالب

على النقيض من التخصيص الكامل للقوالب، يسمح التخصيص الجزئي للقوالب بتقديم قالب مع بعض الوسائط الخاصّة بقالب آخر ثابت. ولا يُتاح التخصيص الجزئي للقوالب إلّا لأصناف وبنيات القالب:

// حالة شائعة
template < typename T, typename U>
    struct S
    {
        T t_val;
        U u_val;
    };
// int  حالة خاصة حيث معامل القالب الأول مثبّت عند النوع
template < typename V>
    struct S<int, V>
    {
        double another_value;
        int foo(double arg)
        {
            // افعل شيئا ما }
        };

كما هو مُوضّح أعلاه، قد تُقدِّم التخصيصات الجزئية للقوالب مجموعات مختلفة تمامًا من البيانات والدوالّ العضوية. عند استنساخ قالب مخصّص جزئيًا، فسيتمّ اختيار التخصيص الأنسب، مثلًا لنعرّف قالبًا مع تخصيصَين جزئِيين:

template < typename T, typename U, typename V>
    struct S
    {
        static void foo()
        {
            std::cout << "General case\n";
        }
    };
template < typename U, typename V>
    struct S<int, U, V>
    {
        static void foo()
        {
            std::cout << "T = int\n";
        }
    };
template < typename V>
    struct S<int, double, V>
    {
        static void foo()
        {
            std::cout << "T = int, U = double\n";
        }
    };

الاستدعاءات التالية:

S<std::string, int, double>::foo();
S<int, float, std::string>::foo();
S<int, double, std::string>::foo();

سوف تَطبع الخرج التالي:

General
case
T = int
T = int, U = double

لا يمُكن أن تُخصّص قوالب الدوالّ جزئيًا:

template < typename T, typename U>
    void foo(T t, U u)
    {
        std::cout << "General case: " << t << " " << u << std::endl;
    }

// حسنا
template < >
    void foo<int, int> (int a1, int a2)
    {
        std::cout << "Two ints: " << a1 << " " << a2 << std::endl;
    }

void invoke_foo()
{
    foo(1, 2.1);    // ==> "General case: 1 2.1" 
    foo(1, 2);    // =>> "Two ints: 1 2" }

// Compilation error: partial function specialization is not allowed.
template < typename U>
    void foo<std::string, U> (std::string t, U u)
    {
        std::cout << "General case: " << t << " " << u << std::endl;
    }

تخصيص القوالب Template Specialization

يمكنك تنفيذ نُسخ مُحدّدة من صنف أو تابع قالب. على سبيل المثال، إذا كان لديك:

template < typename T>
    T sqrt(T t)
    { /*Some generic implementation */ }

فيمكنك إذن كتابة:

template < >
    int sqrt<int> (int i)
    { /* تنفيذ مُحسَّن للأعداد الصحيحة */ }

سيحصل المستخدم الذي يكتُب ‎sqrt(4.0)‎ على التنفيذ العام، بينما يحصل من يكتب ‎sqrt(4)‎ على التنفيذ المُخصّص.

كنى القوالب Alias templates

الإصدار ≥ C++‎ 11

انظر المثال البسيط التالي:

template < typename T > using pointer = T *;

هذا التعريف يجعل ‎pointer<T>‎ كُنيةً (alias) لـ ‎T*‎. مثلًا، يكافئ السطر التالي ;int* p = new int

pointer<int> p = new int; 

لا يمكن تخصيص كُنى القوالب، لكن يمكن جعلها تشير إلى نوع مُتشعِّب في بنية:

template < typename T>
    struct nonconst_pointer_helper
    {
        typedef T * type;
    };
template < typename T>
    struct nonconst_pointer_helper < T
const >
{
    typedef T * type;
};
template < typename T > using nonconst_pointer = nonconst_pointer_helper<T>::type;

الاستنساخ الصريح

سيؤدي تعريف الاستنساخ الصريح إلى إنشاء صنف أو دالة أو متغيّر حقيقي من القالب، كما سيُصرّح عنه قبل استخدامه. يمكن الإشارة إلى تلك النُسخة من وحدات الترجمة الأخرى، ويمكن استخدام ذلك لتجنّب تعريف قالب في ترويسة الملف إذا كان سيُستنُسخ مع مجموعة محدودة من الوسائط. مثلا:

// print_string.h
template < class T>
    void print_string(const T *str);
// print_string.cpp
#include "print_string.h"

template void print_string(const char *);
template void print_string(const wchar_t *);

ونظرًالأنّ ‎print_string<char>‎ و ‎print_string<wchar_t>‎ مُستنسختان بشكل صريح في ‎print_string.cpp‎، فسيتمكّن الرابط (linker) من العثور عليهما رغم أنّ القالب ‎print_string‎ لم يُعرَّف في الترويسة، وإذا لم تكن هذه التصريحات الفورية حاضرة، فمن المحتمل حدوث خطأ في الرابط.

تبيّن هذه الصفحة الأجنبية لماذا لا يمكن تقديم القوالب إلا في الترويسة.

الإصدار ≥ C++‎ 11

إذا سُبِق تعريف الاستنساخ الصريح بالكلمة ‎extern‎، فسيتحوّل إلى تصريح عن استنساخ صريح.

التصريح عن استنساخ صريح لتخصيص معيّن يمنع الاستنساخ الضمني لذلك التخصيص داخل وحدة الترجمة الحالية، لكن لا مانع أن يشير مرجع لذلك التخصيص -الذي كان من الممكن أن يتسبب في استنساخ ضمني- إلى تعريف استنساخ صريح في نفس وحدة الترجمة أو في غيرها.

foo.h‎

#
ifndef FOO_H
#define FOO_H
template < class T > void foo(T x)
{
    // تنفيذ معقّد
}#
endif

foo.cpp

#include "foo.h"
    // تعريف صريح لاستنساخ الحالات الشائعة.
template void foo(int);
template void foo(double);

main.cpp

#include "foo.h"
    // لها تعريف استنساخ صريح foo.cpp نعلم أنّ
extern template void foo(double);
int main()
{

هنا تُستنسخ <foo<int، وذلك لا فائدة منه بما أن foo.cpp تقدم استنساخًا صريحًا سلفًا، نتابع:

    foo(42);

وهنا، لن تُستنسخ <foo<double إذ تستخدم نسخة من <foo<double في foo.cpp بدلًا من ذلك، انظر بقية المثال:

    foo(3.14);   
}

مُعامل قالب غير نوعي

يُسمح لنا بالتصريح عن قيم التعبيرات الثابتة التي تفي بأحد المعايير التالية، وذلك خلا النوع كمعامِل قالب:

  • نوع عددي صحيح أو تعداد (enumeration).
  • مؤشّر إلى كائن أو مؤشّر إلى دالة.
  • مرجع يساري إلى كائن أو مرجع يميني إلى دالّة.
  • مؤشّر إلى عضو.
  • std::nullptr_t.

يمكن تحديد معاملات القالب غير النوعية بشكل صريح -مثل كل معاملات القوالب- أو اشتقاقها أو تحديد قيمها الافتراضية ضمنيًا عبر استنباط وسيط القالب. هذا مثال على استخدام مُعامل قالب غير نوعي، إذ سنمرر مصفوفة بالمرجع تتطلب حجمًا معينًا، ونحن نسمح بكل الأحجام باستخدام قالب size:

#include <iostream>

template < typename T, std::size_t size>
    std::size_t size_of(T(&anArray)[size])  
{
    return size;   
}

int main()
{
    char anArrayOfChar[15];
    std::cout << "anArrayOfChar: " << size_of(anArrayOfChar) << "\n";
    int anArrayOfData[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    std::cout << "anArrayOfData: " << size_of(anArrayOfData) << "\n";
}

انظر المثال التالي على استخدام معاملات القالب النوعية وغير النوعية بشكل صريح، إذ يكون int معامِلًا نوعيًا، أما 5 فلا:

#include <array>
int main()
{
    std::array<int, 5> foo; 
}

تعدّ معاملات القوالب غير النوعية إحدى طرق تحقيق عوديّة القوالب والبرمجة العليا.

التصريح عن وسائط القوالب غير النوعية عبر auto

كان عليك قبل الإصدار C++‎ 17 أن تحدد نوع معامِل القالب غير النوعي عند كتابته، لذلك كان من الشائع كتابة شيء من هذا القبيل:

template < class T, T N>
    struct integral_constant
    {
        using type = T;
        static constexpr T value = N;
    };
using five = integral_constant<int, 5> ;

ولكن بالنسبة للتعبيرات المعقّدة، فإنّ استخدام الصيغة أعلاه يتطّلب كتابة ‎decltype(expr), expr‎ عند استنساخ القوالب، والحلّ هو تبسيط هذا المنظور واستخدام ‎auto‎:

الإصدار ≥ C++‎ 17

template < auto N>
    struct integral_constant
    {
        using type = decltype(N);
        static constexpr type value = N;
    };
using five = integral_constant<5> ;

حاذف مخصّص للمؤشرات الحصرية unique_ptr

من أمثلة استخدام وسائط القوالب غير النوعية هي الجمع بين تحسين الأساس الفارغ وحاذف مُخصَّص للمؤشّرات الحصرية ‎unique_ptr‎. وتختلف حاذفات الواجهات البرمجية للغة C في نوع القيمة المعادة، ولكن هذا لا يشغلنا، فكل ما نريد هو شيء يعمل مع كل الدوالّ:

template < auto DeleteFn>
    struct FunctionDeleter
    {
        template < class T>
            void operator()(T *ptr) const
            {
                DeleteFn(ptr);
            }
    };
template<T, auto DeleteFn>
    using unique_ptr_deleter = std::unique_ptr<T, FunctionDeleter < DeleteFn>> ;

والآن يمكنك استخدام أيّ مؤشّر دالة يقبل وسيطًا من النوع ‎T‎ كمُعامل قالب غير نوعي، بصرف النظر عن نوع القيمة المُعادة، ودون القلق من أيّ حِمل إضافي في الحجم:

unique_ptr_deleter<std::FILE, std::fclose > p;

معامِلات قالب القالب

قد نود أحيانًا أن نمرّر إلى القالب نوعَ قالبٍ دون تحديد قيمه، وهنا يأتي دور معاملات قالب القالب. هذا مثال بسيط يوضّح مفهوم مُعاملات قالب القالب:

template < class T>
    struct Tag1 {};
template < class T>
    struct Tag2 {};
template<template<class> class Tag>
    struct IntTag
    {
        typedef Tag<int> type;
    };
int main()
{
    IntTag<Tag1>::type t;
}

الإصدار ≥ C++‎ 11

#include <vector>
#include <iostream>

template < class T, template < class... > class C, class U >
    C<T> cast_all(const C<U> &c)
    {
        C<T> result(c.begin(), c.end());
        return result;
    }

int main()
{
    std::vector<float> vf = { 1.2, 2.6, 3.7 };
    auto vi = cast_all<int> (vf);
    for (auto && i: vi)
    {
        std::cout << i << std::endl;
    }
}

القيم الافتراضية لمُعاملات القالب

كما في حالة وسائط الدوال، يمكن أن يكون لمعاملات القالب قيمٌ افتراضية، ويجب التصريح عن معاملات القالب ذات القيمة الافتراضية في نهاية قائمة معاملات القالب، والهدف هو إتاحة حذف معاملات القالب ذات القيمة الافتراضية أثناء استنساخ القالب.

هذا مثال بسيط يوضّح كيفية استخدام القيمة الافتراضية لمُعامل القالب:

template < class T, size_t N = 10 >
    struct my_array
    {
        T arr[N];
    };
int main()
{
    /* N = 5 إهمال قيمة المعامل الافتراضية */
    my_array<int, 5> a;

  /* 5 - a.arr اطبع طول */
    std::cout << sizeof(a.arr) / sizeof(int) << std::endl;

    /* N = 10 المعامل الأخير محذوف */
    my_array<int> b;

    /* 10 - a.arr  اطبع طول*/
    std::cout << sizeof(b.arr) / sizeof(int) << std::endl;
}

أنماط القوالب عجيبة التكرار CRTP

أنماط القوالب عجيبة التكرار (Curiously Recurring Template Pattern)، أو CRTP اختصارًا، هي أنماط برمجية يكون من الممكن فيها أن يرث صنف من قالب صنف، بحيث يكون ذلك الصنف نفسه أحد معامِلات القالب. وتُستخدم CRTP عادة لتوفير تعددية الأشكال الساكنة (static polymorphism) في C++‎.

وتُعدّ CRTP بديلًا ممتازًا وساكنًا (static) للدوالّ الوهمية والوراثة التقليدية، إذ يمكن استخدامها لتحديد خصائص الأنواع في وقت التصريف، ويقوم مبدأ عملها على جعل قالب صنف أساسي (base class template) يأخذ صنفًا مشتقًا منه كأحد معاملات القالب خاصته، وهذا يسمح بإجراء تحويل ساكن ‎static_cast‎ للمؤشّر ‎this‎ الخاص بالصنف الأساسي لكي يشير إلى الصنف المشتق.

بالطبع، هذا يعني أنّه سيتوجّب استخدام صنف CRTP دائمًا كصنف أساسي (base class) لصنف آخر، ويجب أن يمرِّر الصنف المشتق نفسه إلى الصنف الأساسي.

الإصدار ≥ C++‎ 14

لنفترض أنّ لديك مجموعة من الحاويات التي تدعم الدالّتين ‎begin()‎ و ‎end()‎، وتتطّلب المكتبة القياسية للحاويات المزيد من الدوالّ. يمكننا هنا أن نصمم صنف CRTP أساسي يوفّر مثل تلك الدولب استنادًا إلى ‎begin()‎ و ‎end()‎ فقط:

#include <iterator>
template < typename Sub>
    class Container
    {
        private:

تعيد ()self مرجعًا إلى النوع المشتق، نتابع المثال:

            Sub &self()
            {
                return * static_cast<Sub*> (this);
            }

        Sub
        const &self() const
        {
            return * static_cast< Sub
            const*> (this);
        }

        public:
            decltype(auto) front()
            {
                return* self().begin();
            }

        decltype(auto) back()
        {
            return *std::prev(self().end());
        }

        decltype(auto) size() const
        {
            return std::distance(self().begin(), self().end());
        }

        decltype(auto) operator[](std::size_t i)
        {
            return *std::next(self().begin(), i);
        }
    };

يوفّر الصنف أعلاه الدوالّ ‎front()‎ و ‎back()‎ و ‎size()‎ و ‎operator[]‎ لأي صنف فرعي يوفر الدالتين ‎begin()‎ و ‎end()‎. في المثال التالي، يمكن للمصفوفات البسيطة المخصّصة ديناميكيًا أن تكون صنفًا فرعيًا:

#include <memory>
    // مصفوفة مخصّصة ديناميكيا
template < typename T>
    class DynArray: public Container<DynArray < T>>
    {
        public: using Base = Container<DynArray < T>> ;
        DynArray(std::size_t size): size_
        {
            size
        },
        data_
        {
            std::make_unique < T[] > (size_)
        } {}

        T* begin()
        {
            return data_.get();
        }

        const T* begin() const
        {
            return data_.get();
        }

        T* end()
        {
            return data_.get() + size_;
        }

        const T* end() const
        {
            return data_.get() + size_;
        }

        private: std::size_t size_;
        std::unique_ptr < T[] > data_;
    };

يمكن الآن لمستخدمي الصنف ‎DynArray‎ استخدام الواجهات التي يوفّرها الصنف الأساسي CRTP بسهولة على النحو التالي:

DynArray<int> arr(10);
arr.front() = 2;
arr[2] = 5;
assert(arr.size() == 10);

فائدة النمط: يتجنّب هذا النمط استدعاءات الدوالّ الوهمية في وقت التشغيل، والتي تسعى لاجتياز التسلسل الهرمي للوراثة، ويعتمد بدلًا من ذلك على التحويلات الساكنة (static casts):

DynArray<int> arr(10);
DynArray<int>::Base &base = arr;
base.begin();    // لا استدعاءات وهمية

ويسمح التحويل الساكن الوحيد داخل الدالّة ‎begin()‎ في الصنف الأساسي ‎Container<DynArray<int>>‎ للمٌصرّف بتحسين الشيفرة بشكل كبير، إذ لن يحدث أي تنقيب في الجدول الوهمي (virtual table) في وقت التشغيل.

عيوب النمط: نظرًا لأنّ الصنف الأساسي مُقوْلَب ويختلف من مصفوفة ‎DynArray‎ إلى أخرى، فلا يمكن تخزين المؤشّرات التي تشير إلى أصنافها الأساسية في مصفوفة متجانسة كما نفعل عمومًا مع الوراثة العادية التي لا يعتمد فيها الصنف الأساسي على الصنف المشتق:

class A {};
class B: public A {};
A *a = new B;

استخدام أنماط CRTP لتجنّب تكرار الشيفرة

يوضّح المثال التالي كيفية استخدام CRTP لتجنّب تكرار الشيفرة:

struct IShape
{
    virtual~IShape() = default;
    virtual void accept(IShapeVisitor &) const = 0;
};
struct Circle: IShape
{

يجب على كل شكل أن يقدم هذا التابع بنفس الطريقة … ، نتابع المثال:

    void accept(IShapeVisitor & visitor) const override
    {
        visitor.visit(*this);
    }

    // ...
};
struct Square: IShape
{

بالمثل هنا، يجب على كل شكل أن يقدم هذا التابع بنفس الطريقة، نتابع:

    void accept(IShapeVisitor & visitor) const override
    {
        visitor.visit(*this);
    }

    // ...
};

يحتاج كل نوع فرعي من ‎IShape‎ إلى تنفيذ نفس الدالّة بنفس الطريقة، وهذا قد يأخذ الكثير من الوقت. الحل البديل هو تنفيذ نوع جديد في الهرمية الوراثية يتكفّل بفعل ذلك نيابة عنّا:

template < class Derived>
    struct IShapeAcceptor: IShape
    {
        void accept(IShapeVisitor & visitor) const override
        {
            visitor.visit(*static_cast< Derived
                const*> (this));
        }
    };

والآن، يكفي أن ترث الأشكال من المتقبِّل (acceptor):

struct Circle: IShapeAcceptor < Circle>
{
    Circle(const Point &center, double radius): center(center), radius(radius) {}

    Point center;
    double radius;
};
struct Square: IShapeAcceptor < Square>
{
    Square(const Point &topLeft, double sideLength): topLeft(topLeft), sideLength(sideLength) {}

    Point topLeft;
    double sideLength;
};

لم تعد هناك حاجة الآن إلى تكرار الشيفرة.

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 78: Expression templates والفصل Chapter 79: Curiously Recurring Template Pattern (CRTP)‎ من كتاب C++ Notes for Professionals


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...