أربعة عوامل تحويل صريحة بـ C++، وكل مبتدئ يمر بلحظة "شو الفرق بينهم فعليًا ومتى أستخدم كل واحد؟" هذا المقال يقارنها جنبًا لجنب مع أمثلة عملية.
الفرق بلمحة
| العامل | الاستخدام النموذجي | تحقّق وقت التشغيل؟ | الخطورة |
|---|---|---|---|
static_cast | أرقام، تعدادات، تسلسل وراثي معروف | لا | متوسطة |
dynamic_cast | تنزيل آمن بتسلسل فيه virtual | نعم | منخفضة |
const_cast | إضافة/إزالة const/volatile فقط | لا | متوسطة |
reinterpret_cast | إعادة تفسير بتّات منخفضة المستوى | لا | عالية جدًا |
static_cast: "أنا متأكّد من هذا التحويل"
يُستخدم لتحويلات منطقية بين أنواع مرتبطة — رقمية، تعدادات، أو مؤشّرات ضمن تسلسل وراثي واحد. المترجم لا يتحقّق وقت التشغيل من صحة تنزيل مؤشّر؛ يثق بحكمك أنت:
class Animal {};
class Dog : public Animal {};
Animal* a = new Dog();
Dog* d = static_cast<Dog*>(a); // بلا تحقّق — لو a ليس فعليًا Dog، سلوك غير معرَّف
dynamic_cast: "تأكّد لي فعليًا وقت التشغيل"
الفرق الجوهري: يتحقّق فعليًا من نوع الكائن وقت التشغيل، وله ثمن أداء
مقابل ذلك، ويتطلّب أن يملك الصنف الأساس دالة virtual واحدة على الأقل:
class Shape { public: virtual ~Shape() = default; };
class Circle : public Shape {};
Shape* s = new Circle();
if (Circle* c = dynamic_cast<Circle*>(s)) {
// نجح فعليًا — c مؤكَّد أنه يشير لـ Circle حقيقي
} else {
// فشل بهدوء — c هنا nullptr، بلا انهيار
}
💡 القاعدة العملية: استخدم
static_castلما تكون متأكّدًا منطقيًا من صحة التحويل (أو لا تحتاج تنزيلًا أصلًا)، واحفظdynamic_castلحالات التنزيل الفعلي غير المؤكّد وقت الترجمة — رغم كلفته الإضافية.
const_cast: العامل المتخصّص الوحيد
على عكس الثلاثة الأخرى، لا يغيّر النوع الأساسي إطلاقًا — فقط صفة const
أو volatile. آمن فقط لو الكائن الأصلي لم يُعرَّف const من الأساس:
void legacyPrint(char* text) { /* دالة قديمة لا تقبل const char* */ }
void printMessage(const char* msg) {
legacyPrint(const_cast<char*>(msg)); // نزيل const لمطابقة التوقيع فقط
}
reinterpret_cast: الأخطر بينهم جميعًا
لا يوجد أي منطق تحويل حقيقي هنا — فقط إعادة تسمية لنفس بتّات الذاكرة كنوع آخر تمامًا، بلا أي علاقة بين النوعين:
int value = 65;
int* p = &value;
uintptr_t address = reinterpret_cast<uintptr_t>(p); // مؤشّر إلى عدد صحيح
char* bytes = reinterpret_cast<char*>(p); // مؤشّرين غير مرتبطين
مخصّص لحالات منخفضة المستوى فقط (أنظمة، شبكات، hashing). لو وجدت نفسك تحتاجه بكود تطبيقات عادي، هذا شبه مؤكّد إشارة لمشكلة تصميم أعمق تستحق إعادة نظر بدل التحايل عليها.
لماذا لا نستخدم (Type)value القديمة إذًا؟
لأنها غامضة: (int)value أو (Dog*)animalPtr قد تنفّذ أيًّا من العوامل
الأربعة أعلاه — أو مزيجًا منها — باختيار صامت من المترجم حسب السياق، دون
أن يوضّح السطر نفسه أي نوع تحويل حصل فعليًا. إرشادات C++ Core
Guidelines الرسمية (القاعدتان ES.48 وES.49) توصي صراحة بتجنّبها لصالح
العوامل الأربعة الصريحة: كل واحد يوثّق نيّتك بوضوح، وأسهل بالبحث عبر
الكود الكبير (grep "static_cast" أعطاك كل التحويلات الرقمية مثلًا، بعكس
البحث عن أقواس عادية).
الخلاصة
اسأل نفسك بالترتيب: هل التحويل منطقي ومعروف وقت الترجمة؟ → static_cast.
هل يحتاج تحقّقًا فعليًا وقت التشغيل بتسلسل فيه تعدّد أشكال؟ → dynamic_cast.
هل المطلوب فقط تعديل const/volatile؟ → const_cast. هل لا يوجد خيار
آخر بحالة منخفضة المستوى؟ → reinterpret_cast، بحذر شديد.
اقرأ الشرح الكامل بدرس التحويل بين الأنواع بمسار C++، أو راجع كل عامل بمرجع C++: static_cast، dynamic_cast، const_cast، وreinterpret_cast.