سلسلة ++c للمحترفين الدرس 22: التحميل الزائد Overloading: تحليله وتطبيقه على الدوال في Cpp


محمد الميداوي

تصنيف كلفة تمرير وسيط إلى معامل

يقسم تحليل التحميل الزائد تكلفة تمرير وسيط (argument) إلى مُعامل (parameter) إلى 4 تصنيفات مختلفة، تُسمّى "تسلسلات" (sequences)، وقد يتضمن كل تسلسل صفرًا أو واحدًا أو عدّة تحويلات.

  • تسلسل التحويل القياسي Standard conversion sequence
void f(int a);
f(42);
  • تسلسل التحويل المُعرّف من قبل المستخدم User defined conversion sequence
void f(std::string s);
f("hello");
  • تسلسل تحويل علامة الحذف Ellipsis conversion sequence:
void f(...);
f(42);
  • تسلسل قائمة التهيئة:
void f(std::vector<int> v);
f({ 1, 2, 3 });

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

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

الترقيات والتحويلات الحسابية Arithmetic promotions and conversions

يُعدّ تحويل نوع عددي صحيح إلى نوع مُرقّى (promoted type) مقابل له أفضل من تحويله إلى نوع عددِي صحيح آخر.

void f(int x);
void f(short x);
signed char c = 42;
f(c);    // short  أفضل من التحويل إلى int الترقية إلى
short s = 42;
f(s);    // int التطابق التام أفضل من الترقية إلى

كذلك ترقية ‎float‎ إلى ‎double‎ أفضل من تحويله إلى نوع عددِي عشري آخر.

void f(double x);
void f(long double x);
f(3.14 f);    // calls f(double); long double أفضل من التحويل إلى double الترقية إلى

تتكافأ التحويلات الحسابية الأخرى، بخلاف الترقيات.

void f(float x);
void f(long double x);
f(3.14);    // غامض
void g(long x);
void g(long double x);
g(42);    // غامض
g(3.14);    // غامض

لذلك، من أجل ضمان رفع أيّ لبس عند استدعاء دالة ‎f‎ سواء مع وسائط عددية صحيحة أو عشرية من أيّ نوع قياسي، فستحتاج لكتابة ثمانية تحميلات زائدة، بحيث يحدث تطابق تام مع التحميل الزائد أو يُختار التحميل الزائد ذو نوع الوسيط المُرقّى (promoted argument type)، مهما كان نوع الوسيط.

void f(int x);
void f(unsigned int x);
void f(long x);
void f(unsigned long x);
void f(long long x);
void f(unsigned long long x);
void f(double x);
void f(long double x);

التحميل الزائد على مرجع إعادة توجيه Forwarding Reference

يجب أن تكون حذرًا عند توفير تحميل زائد لمرجع إعادة توجيه (forwarding reference) لأنّه قد يكون تامَّ التطابق:

struct A {
A() = default; // #1
A(A const& ) = default; // #2
template <class T>
A(T&& ); // #3
};

والقصد هنا هو أنّ ‎A‎ قابلة للنسخ، وأنّ لدينا منشئًا آخر يمكنه أن يهيّئ عضوًا آخر. انظر:

A a;    // #1 استدعاء
A b(a);    // #3! استدعاء

هناك تطابُقان صالحان لاستدعاء الإنشاء:

A(A const& ); // #2
A(A& );           // #3, مع T = A&

كلاهما مُطابِقان بشكل تام، لكن ‎#3‎ تأخذ مرجعًا إلى كائن تأهيله أقل من ‎#2‎، لذا فهو أكثر امتثالًا لتسلسل التحويل القياسي، وأكثر تطابقًا، والحل هنا هو تقييد هذه المنشآت دائمًا (على سبيل المثال باستخدام قاعدة SFINAE):

template <class T,
class = std::enable_if_t<!std::is_convertible<std::decay_t<T>*, A*>::value> > A(T&& );

وما نريده هنا هو استبعاد أيّ نوع ‎A‎ أو صنف مُشتقّ علنًا (publicly) من ‎A‎ من نظرنا، ونتيجة لذلك تصبح صيغة هذا المنشئ غير صحيحة في المثال المُوضّح سابقًا (وعليه يُزال من مجموعة التحميل الزائد). لذا سيُستدعى مُنشئ النسخ، وهو الذي أردنا.

التطابق التام

يفضَّل التحميل الزائد الذي لا يحتاج إلى تحويل أنواع المعاملات أو الذي يحتاج فقط إلى التحويلات بين الأنواع التي تُعدُّ مُتطابقة بشكل تام، على التحميل الزائد الذي يتطّلب تحويلات أخرى قبل إجراء الاستدعاء.

void f(int x);
void f(double x);
f(42);    // f(int) استدعاء

عندما يرتبط وسيط بمرجع من نفس النوع، فإنّ المطابقة لن تتطّلب تحويلًا حتى لو كان المرجع ذا تأهيل ثباتي أعلى. انظر المثال التالي حيث يكون نوع الوسيط في (f(x هو int، وهو تطابق تام مع &int:

void f(int& x);
void f(double x);
int x = 42;
f(x);   
void g(const int& x);
void g(int x);
g(x);    // غامض، كلا التحميلين الزائدين يعطيان تطابقا تاما

يعد النوعان "مصفوفة تحتوي عناصر من النوع ‎T‎" و "مؤشّر إلى ‎T‎" متطابقين بشكل تام لغرض تحليل التحميل الزائد، كما يُعدُّ نوع الدالّة ‎T‎ مطابقًا تمامًا لنوع مؤشّر الدالّة ‎T*‎، رغم أنّ كليهما يتطّلبان إجراء تحويلات .

void f(int* p);
void f(void* p);
void g(int* p);
void g(int (&p)[100]);
int a[100];
f(a);    // f(int*); تطابق تام، مع تحويل من مصفوفة إلى مؤشّر
g(a);    // غامض، كلا التحويلين الزائدين يعطيان تطابقا تاما

التحميل الزائد للثباتية constness والتغير volatility

يُعدّ تمرير وسيط مؤشّر (pointer argument) إلى مُعامل ‎T*‎ -إن أمكن- أفضل من تمريره إلى مُعامل ‎const T*‎.

struct Base {};
struct Derived : Base {};
void f(Base* pb);
void f(const Base* pb);
void f(const Derived* pd);
void f(bool b);
Base b;
f(&b);  
Derived d;
f(&d);    

تفسير الشيفرة السابقة:

  • في (f(&b: تٌفضَّل (*f(base على (*f(const base.
  • في (f(&d: تٌفضَّل (*f(const Derived على (*f(base، رغم أن دور الثباتية ينحصر في كسر التعادل (tie-breaker).

وبالمثل، يُعدّ تمرير وسيط إلى مُعامل ‎T&‎ أفضل من تمريره إلى مُعامل ‎const T&‎، حتى لو كان لكليهما نفس التطابق (match rank).

void f(int& r);
void f(const int& r);
int x;
f(x);    // أفضل f(int&) التحميلان مطابقان، لكنّ
const int y = 42;
f(y);    // هي المرشح الصالح f(const int&) 

تنطبق هذه القاعدة أيضًا على الدوال التابعة المُؤهّلة ثباتيًا (const-qualified member functions)، التي تحتاج إلى السماح بالوصول المتغيّر (mutable access) إلى الكائنات غير الثابتة، والوصول الثابت (immutable access) إلى الكائنات الثابتة.

class IntVector
{
    public:
        // ...
       int* data() { return m_data; }
       const int* data() const { return m_data; }

    private:
        // ...
        int* m_data;
};
IntVector v1;
int* data1 = v1.data(); 
const IntVector v2;
const int* data2 = v2.data();   

تفسير الشيفرة السابقة:

  • ()Vector::data أفضل من Vector::data() const، ويمكن استخدام data1 لتعديل المتجه.
  • Vector::data() const هي المرشح الصالح الوحيد، ولايمكن استخدام data2 لتعديل المتجه.

كذلك فإن التحميل الزائد غير المتغيّر (non-volatile overload) أفضل من التحميل الزائد المتغيّر:

class AtomicInt
{
    public:
        // ...
        int load();
    int load() volatile;
    private:
        // ...
};
AtomicInt a1;
a1.load();    // يُفضَّل التحميل الزائد غير المتغير، لايوجد آثار جانبية
volatile AtomicInt a2;
a2.load();    // التحويل الزائد المتغير هو الوحيد المرشّح، مع وجود آثار جانبية
static_cast< volatile AtomicInt &> (a1).load();   

البحث عن الاسماء والتحقق من الوصول

يحدث تحليل التحميل الزائد بعد البحث عن الاسم، هذا يعني أنّ تحليل التحميل الزائد قد لا يختار الدالّة الأكثر تطابقا في حال عدم العثور على اسمها. انظر المثال التالي حيث نستدعي S::f لأن f غير مرئية هنا، رغم أنها ستكون أكثر تطابقًا.

void f(int x);
struct S
{
    void f(double x);
    void g()
    {
        f(42);
    }  
};

يحدث تحليل التحميل الزائد قبل التحقّق من الوصول، وقد تُختار دالّة غير مُتاحة للوصول من قبل تحليل التحميل الزائد بدلًا من دالة أخرى أقلّ تطابقًا ولو كانت متاحة للوصول.

class C
{
    public:
        static void f(double x);
    private:
        static void f(int x);
};
C::f(42);

في الشيفرة السابقة، تعطي (C::f(42 خطأً، لأنها تستدعي (private C::f(int رغم أن (public C::f(double` صالحة.

وبالمثل، لا يتحقّق تحليل التحميل الزائد ممّا إذا كانت صيغة ‎explicit‎ صحيحة في الاستدعاء الناتج:

struct X
{
    explicit X(int);
    X(char);
};
void foo(X);
foo({ 4 });    // أكثر تطابقا، لكن التعبير سيء الصياغة، لأنّ المنشئ صريح X(int) 

التحميل الزائد داخل التسلسل الهرمي للصنف

الأمثلة التالية سوف تستخدم هذا التسلسل الهرمي:

struct A
{
    int m;
};
struct B: A {};
struct C: B {};

يُفضل التحويل من نوع صنف مشتق (derived class type) إلى نوع صنف أساسي (base class type) في التحويلات المُعرَّفة من قبل المستخدم، ينطبق هذا عند التمرير بالقيمة (by value) أو بالمرجع (by reference)، وكذلك عند تحويل مؤشّر يشير إلى صنف مشتق إلى مؤشّر آخر يشير إلى صنف أساسي.

struct Unrelated {
Unrelated(B b);
};
void f(A a);
void f(Unrelated u);
B b;
f(b);  // f(A) استدعاء

يُعدّ تحويل المؤشّر من صنف مشتق إلى صنف أساسي أفضل من تحويله إلى ‎void*‎.

void f(A* p);
void f(void* p);
B b;
f(&b);    // f(A*) استدعاء

إذا كانت هناك عدّة تحميلات زائدة داخل نفس التسلسل الوراثي، فستكون الأولوية للتحميل الزائد الخاص بالصنف الأساسي الأكثر اشتقاقا (most derived base class). هذا المبدأ مماثل للإرسال الوهمي (virtual dispatch)، إذ يُختار التنفيذ "الأكثر تخصّصًا"، لكن يحدث تحليل التحميل الزائد دائمًا في وقت التصريف، ولن يُنزّل (down-cast) ضمنيًا أبدًا.

void f(const A& a);
void f(const B& b);
C c;
f(c);    // f(const B&) استدعاء
B b;
A& r = b;
f(r);    // f(const A&) استدعاء
    // غير صالح f(const B&) التحميل الزائد لـ  

بالنسبة للمؤشّرات العضوية (pointers to members)، تُطبّق قاعدة مماثلة ولكن في الاتجاه المعاكس، إذ يُفضَّل الصنف المشتق الأقل اشتقاقا (least derived derived class).

void f(int B::*p);
void f(int C::*p);
int A::*p = &A::m;
f(p);   // f(int B::*) استدعاء

خطوات تحليل التحميل الزائد

خطوات تحليل التحميل الزائد هي:

  1. البحث عن الدوالّ المرشّحة عبر البحث بالاسم. ستُجري الاستدعاءات غير المُؤهّلة كلّا من البحث العادي غير المُؤهّل - regular unqualified lookup - وكذلك البحث القائم على الوسيط - argument-dependent lookup - (إن أمكن).
  2. غربلة مجموعة الدوالّ المرشّحة لاستخلاص مجموعة من الدوالّ القابلة للتطبيق. الدوالّ القابلة للتطبيق هي الدوال التي يوجد تسلسل تحويل ضمني بين الوسائط المُمرّرة إليها والمعاملات التي تقبلها. في المثال التالي، تكون الدالتان 1 و2 صالحتين رغم حذفنا للدالة 2، والدالة 3 غير صالحة لعدم تطابق قائمة الوسائط، أما 4 فغير صالحة لأننا لا نستطيع ربط عنصر مؤقت بمرجع يساري غير ثابت.
void f(char);    // (1)
void f(int) = delete;    // (2)
void f();    // (3)
void f(int&);    // (4)
f(4);   
  1. اختيار أفضل دالّة مرشحة قابلة للتطبيق. تكون دالّة قابلة للتطبيق ‎F1‎ أفضل من دالّة أخرى أخرى قابلة للتطبيق ‎F2‎ إذا لم يكن تسلسل التحويل الضمني لكل وسيط في ‎F1‎ أسوأ من تسلسل التحويل الضمني المقابل في ‎F2‎، 
    • وبالنسبذة لوسيط ما، فإنّ تسلسل التحويل الضمني لذلك الوسيط في ‎F1‎ أفضل من تسلسل تحويل ذلك الوسيط في ‎F2‎،
void f(int);    // (1)
void f(char);    // (2)
f(4);    // استدعاء 1 لأنّ تسلسل التحويل فيها أفضل

 

  • أو في التحويلات المُعرَّفة من قبل المستخدم، فإنّ تسلسل التحويل القياسي (standard conversion sequence) من القيمة المُعادة من ‎F1‎ إلى النوع المقصود أفضل منه لدى نوع القيمة المُعادة من ‎F2‎،
struct A
{
    operator int();
    operator double();
}

a;
int i = a;  
float f = a;    // غامض

تفسير int i = a في الشيفرة السابقة: يفضَّل ()a.operator int على ()a.operator double

  • أو يكون‏ لـ ‎F1‎ نفس نوع المرجع في ارتباط مرجعي مباشر (direct reference binding)، على خلاف ‎F2‎،
struct A
{
    operator X&();    // #1
    operator X&&();   // #2
};
A a;
X& lx = a;    // #1 استدعاء
X&& rx = a;    // #2 استدعاء

 

  • أو ‎F1‎ ليست تخصيصًا لقالب دالة، على خلاف ‎F2‎،
template < class T > void f(T);    // #1
void f(int);    // #2
f(42);    // #2 استدعاء

 

  • أو ‎F1‎ و ‎F2‎ كلاهما تخصيصان لقالب الدالّة، بيْد أنّ ‎F1‎ أكثر تخصيصًا من ‎F2‎.
template < class T > void f(T);    // #1
template < class T > void f(T*);    // #2
int* p;
f(p);    // #2 استدعاء

الترتيب هنا مهم، فالتحقّق من تسلسل التحويل قبل القالب أفضل مقارنة بعدم التحقق من القالب (non-template check)، إذ يؤدّي هذا إلى خطأ شائع في التحميل الزائد لمراجع إعادة التوجيه (forwarding reference):

struct A
{
    A(A const& );    // #1

    template < class T>
        A(T&&);    //  #2 ليست مقيدة
};
A a;
A b(a);    // #2 استدعاء

تفسير الشيفرة السابقة: الحالة 1 ليست قالبًا، والحالة 2 تحلَّل إلى (&A(A التي تكون مرجعًا أقل تأهيلًا مقارنة بحالة 1، وذلك يجعلها الخيار الأنسب لتسلسل التحويل الضمني.

إذا لم يكن هناك مرشّح قابل للتطبيق أفضل من غيره، فسيُعدُّ الاستدعاء غامضًا:

void f(double) {}

void f(float) {}

f(42);    // خطأ: غامض

التحميل الزائد للدوال (Function Overloading)

ما هو التحميل الزائد للدوال؟

التحميل الزائد للدوالّ هو أن يُصرَّح عن عدة دوّال تحمل نفس الاسم بالضبط ولها نفس النطاق، وتختلف فقط في بصمتها (signature)، أي في الوسائط التي تقبلها.

لنفترض أنّك تريد كتابة سلسلة من الدوالّ المتخصّصة في الطباعة بشكل عام، بدءًا بالسّلاسل النصية std::string:

void print(const std::string & str) {
    std::cout << "This is a string: " << str << std::endl;
}

سيعمل هذا بكفاءة، لنفترض الآن أنّك تريد دالّة تقبل عددًا صحيحًا (‎int‎) وتطبعه. يمكنك كتابة:

void print_int(int num) {
    std::cout << "This is an int:  " << num << std::endl;
}

ولكن بما أن الدالتان تقبلان معاملات مختلفة، فيمكنك ببساطة كتابة:

void print(int num) {
    std::cout << "This is an int: " << num << std::endl;
}

صار لدينا الآن دالتان، كلتاهما تحمل الاسم ‎print‎ ولكن مع بصْمتين مختلفين، وتقبل إحداهما سلسلة نصية، والأخرى تقبل عددًا صحيحًا (‎int‎). يمكنك الآن استدعَاؤهما على النحو التالي:

print("Hello world!"); //  "This is a string: Hello world!"
print(1337); //  "This is an int: 1337"

بدلًا من:

print("Hello world!");
print_int(1337);

عندما تكون لديك عدة دوالّ زائدة التحميل (overloaded)، سيستنتج المُصرّف أيًّا من تلك الدوالّ يجب عليه استدعاؤها عبر تحليل المعاملات المُمرّرة. كذلك يجب الحذر عند تحميل الدوالّ. انظر المثال التالي مع تحويلات النوع الضمنية (implicit type conversions):

void print(int num) {
    std::cout << "This is an int: " << num << std::endl;
}
void print(double num) {
    std::cout << "This is a double: " << num << std::endl;
}

كما ترى فليس من الواضح أيُّ تحميلٍ زائد للدالّة ‎print‎ سيُستدعى عند كتابة:

print(5);

وقد تحتاج إلى إعطاء المُصرّف بعض التلميحات، مثل:

print(static_cast<double>(5));
print(static_cast<int>(5));
print(5.0);

يجب توخّي الحذر أيضًا عند كتابة تحميلات زائدة تقبل معامِلات اختيارية:

// انتبه! شيفرة خاطئة
void print(int num1, int num2 = 0) // هي 0 num2 القيمة الافتراضية لـ
{
    std::cout << "These are ints: << num1 << "
    and " << num2 << std::endl;
}
void print(int num) {
    std::cout << "This is an int: " << num << std::endl;
}

سيعجز المُصرّف عن معرفة إن كان الاستدعاء ‎print(17)‎ مخصّصًا للدالّة الأولى أو الثانية، وذلك بسبب المعامل الثاني الاختياري، لهذا لن تُصرّف هذه الشيفرة.

نوع القيمة المعادة في الدوالّ زائدة التحميل

لاحظ أنك لا تستطيع زيادة التحميل على دالّة بناءً على نوع قيمتها المعادة. مثلا:

// شيفرة خاطئة
std::string getValue() {
    return "hello";
}
int getValue() {
    return 0;
}
int x = getValue();

سيؤدي هذا إلى حدوث خطأ في التصريف نظرًا لأنّ المصرّف سيعجز عن تحديد نسخة ‎getValue‎ التي يجب استدعاؤها، رغم اختلاف نوع القيمة المعادة في التصريحين.

زيادة تحميل الدوال التابعة المؤهلة

يمكن زيادة تحميل الدوال التي داخل الأصناف عند الوصول إليها عبر مراجع مؤهَّلة (cv-qualified reference) لذلك الصنف، ويُستخدم هذا غالبًا لزيادة تحميل الثوابت (‎const‎)، ولكن يمكن استخدامها أيضًا لزيادة تحميل القيم ذات التأهيل ‎volatile‎ و ‎const ‎volatile‎ أيضًا، ذلك لأنّ جميع الدوال التابعة غير الساكنة تأخذ this كمعامِل خفي، ذلك المعامِل تُطبَّقُ عليه المؤهّلات الثباتية (cv- qualifiers).

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

يمكن لصنف ذي تابع ‎print‎ أن يُزاد تحميله ثباتيًا (const overloaded) على النحو التالي:

#include <iostream>

class Integer
{
public:
Integer(int i_): i{i_}{}
void print()
{
std::cout << "int: " << i << std::endl;
}

void print() const
{
std::cout << "const int: " << i << std::endl;
}
protected:
int i;
};
int main()
{
Integer i{5};
const Integer &ic = i;

i.print(); // يطبع "int: 5"
ic.print(); // يطبع "const int: 5"
}

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

class ConstCorrect {
    public:
        void good_func() const {
            std::cout << "I care not whether the instance is const." << std::endl;
        }
    void bad_func() {
        std::cout << "I can only be called on non-const, non-volatile instances." << std::endl;
    }
};
void i_change_no_state(const ConstCorrect & cc) {
    std::cout << "I can take either a const or a non-const ConstCorrect." << std::endl;
    cc.good_func(); // جيد، يمكن استدعاؤها من النسخ الثابتة أو غير الثابتة
    cc.bad_func(); // خطأ، لا يمكن استدعاؤها إلا من النسخ غير الثابتة
}
void const_incorrect_func(ConstCorrect & cc) {
    cc.good_func(); // جيد، يمكن استدعاؤها من النسخ الثابتة أو غير الثابتة
    cc.bad_func(); // جيد، لا يمكن استدعاؤها إلا من النسخ غير الثابتة
}

أحد الاستخدامات الشائعة لهذا هو إعلان توابع الوصول (accessors) كثوابت، وتوابع التغيير (mutators) على أنها غير ثابتة، ولا يمكن تعديل عضو من الصنف داخل تابع ثابت. أما إذا كان هناك عضو تحتاج إلى تعديله، كأن تريد قفل std::mutex مثلًا فيمكنك إعلانه كـ ‎mutable‎:

class Integer {
    public:
        Integer(int i_): i {
            i_
        } {}
    int get() const {
        std::lock_guard < std::mutex > lock {
            mut
        };
        return i;
    }
    void set(int i_) {
        std::lock_guard < std::mutex > lock {
            mut
        };
        i = i_;
    }
    protected:
        int i;
    mutable std::mutex mut;
};

زيادة تحميل قوالب الدوال (Function Template Overloading)

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

  • نوع القيمة المعادة مختلفًا، أو …
  • إذا كانت قائمة معاملات القالب مختلفة، باستثناء أسماء المعاملات والوسائط الافتراضية (فهُما ليسا جزءًا من التوقيع).

تبدو مقارنة نوعا معامِلات بالنسبة لدالة عادية عمليةً سهلة على المُصرّف لأنه يملك كل المعلومات اللازمة، ولكن ربما لا تكون الأنواع داخل القالب قد حُدّدت بعد، لذا فإن القاعدة التي تحكم على مدى تطابق نوعي معامِلات تقريبية هنا، وتقضي بأن الأنواع والقيم المستقلة (non depependend) يجب أن تطابق تهجِئة الأنواع والعبارات التابعة (dependent)، أي يجب أن يتوافقًا مع قاعدة التعريف الواحد [ODR]،مع استثناء أن معامِلات القالب يمكن إعادة تسميتها. لكن إذا كانت التهجئة مختلفة، واختلفت قيمتان داخل النوعين لكنهما يستنسخان دائمًا إلى نفس القيم، فتكون زيادة التحميل غير صالحة (invalid)، لكن لن يكون التشخيص مطلوبًا من المصرِّف.

template < typename T >
    void f(T*) { }
template < typename T >
    void f(T) {}

هذا تحميل صالح (valid)، لأنّ "T" و "T *" لهما تهجئتان مختلفتان، لكنّ زيادة التحميل التالية غير صالحة، كما أنّ التشخيص غير مطلوب.

template < typename T >
    void f(T (*x)[sizeof(T) + sizeof(T)]) { }
template < typename T >
    void f(T (*x)[2 * sizeof(T)]) { }

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

ترجمة -بتصرّف- للفصول:

  • Chapter 35: Function Overloading
  • Chapter 37: Function Template Overloading
  • Chapter 105: Overload resolution

من كتاب C++ Notes for Professionals





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن