ليش نحتاج تحويلًا صريحًا أصلًا؟
بعض التحويلات بين الأنواع تحصل تلقائيًا (implicit) — مثل int إلى double.
لكن أحيانًا تريد تحويلًا لا يحصل تلقائيًا، أو تريد إجباره رغم خطورته. بلغة C
كان الحل عامل تحويل واحد غامض:
double price = 19.99;
int cents = (int)(price * 100); // C-style cast — يعمل لكن لا يوضّح نيّتك
المشكلة: (int) قد يعني تحويلًا رقميًا آمنًا، أو قد يعني إعادة تفسير بتّات
الذاكرة بشكل خطير — ولا تعرف أيّهما من مجرّد قراءة السطر. C++ تحلّ هذا بأربعة
عوامل تحويل صريحة، كل واحد له نيّة واضحة ومترجم يتحقّق منها بقدر معيّن.
static_cast — التحويل المعروف وقت الترجمة
الأكثر استخدامًا. يحوّل بين أنواع "مرتبطة منطقيًا" — أرقام، تعدادات، ومؤشّرات ضمن تسلسل وراثي واحد. لا يقوم بأي تحقّق وقت التشغيل؛ أنت من يضمن أن التحويل صحيح:
double price = 19.99;
int cents = static_cast<int>(price * 100); // نفس النتيجة، لكن النيّة واضحة
int code = static_cast<int>(SomeEnum::Active); // enum إلى int
class Animal {};
class Dog : public Animal {};
Dog d;
Animal* a = &d; // تصعيد آمن ضمني أصلًا
Dog* d2 = static_cast<Dog*>(a); // تنزيل — بلا تحقّق: لازم تتأكّد بنفسك أن a فعلًا Dog
dynamic_cast — التنزيل الآمن وقت التشغيل
مخصّص للتنزيل (downcast) بتسلسل وراثي فيه تعدّد أشكال — أي أن الصنف الأساس
يملك دالة virtual واحدة على الأقل. يتحقّق وقت التشغيل فعليًا من نوع الكائن:
class Shape { public: virtual ~Shape() = default; };
class Circle : public Shape { public: void draw() { /* ... */ } };
class Square : public Shape {};
void render(Shape* s) {
Circle* c = dynamic_cast<Circle*>(s);
if (c) { // نجح فقط لو s يشير فعليًا لـ Circle
c->draw();
} else {
// فشل التحويل — c هنا nullptr، بلا انهيار
}
}
مع المؤشّرات، الفشل يُرجع nullptr. مع المراجع، الفشل يرمي استثناء
std::bad_cast لأنه لا يوجد "مرجع فارغ" يُرجعه بدلًا من ذلك.
const_cast — إضافة أو إزالة const
العامل الوحيد المخصّص لتعديل صفة const/volatile فقط، دون تغيير النوع
الأساسي:
void legacyPrint(char* text) { /* دالة قديمة لا تقبل const char* */ }
void printMessage(const char* msg) {
legacyPrint(const_cast<char*>(msg)); // نزيل const لنطابق توقيع الدالة القديمة
}
💡 استخدامه آمن فقط لو الكائن الأصلي لم يُعرَّف
constأصلًا (كما بالمثال أعلاه، حيثmsgمجرّد معامل بمرجع ثابت). تعديل قيمة أُنشئتconstمن الأساس عبرconst_castسلوك غير معرَّف (undefined behavior) — قد يعمل بالصدفة وينهار لاحقًا.
reinterpret_cast — إعادة تفسير البتّات
الأخطر والأقل أمانًا: يعيد تفسير نمط البتّات كنوع مختلف تمامًا، بلا أي علاقة منطقية بين النوعين. مفيد فقط بحالات منخفضة المستوى (تشفير، أنظمة، شبكات):
int value = 65;
int* p = &value;
// تحويل مؤشّر إلى عدد صحيح — للتجزئة (hashing) مثلًا
uintptr_t address = reinterpret_cast<uintptr_t>(p);
// تحويل بين نوعي مؤشّرات غير مرتبطين إطلاقًا
char* bytes = reinterpret_cast<char*>(p);
جدول المقارنة
| العامل | متى يُستخدم | تحقّق وقت التشغيل؟ |
|---|---|---|
static_cast | أرقام، تعدادات، تسلسل وراثي معروف | لا |
dynamic_cast | تنزيل آمن بتسلسل فيه virtual | نعم |
const_cast | إضافة/إزالة const أو volatile فقط | لا |
reinterpret_cast | إعادة تفسير بتّات منخفضة المستوى | لا |
لماذا نتجنّب (Type)value القديمة
إرشادات C++ Core Guidelines الرسمية (قاعدتا ES.48 وES.49) توصي صراحة بتجنّب
الـ C-style cast واستخدام أحد العوامل الأربعة أعلاه بدلها. السبب: (int)
القديمة قد تنفّذ static_cast أو const_cast أو reinterpret_cast — أو مزيجًا
منها — باختيار صامت من المترجم حسب السياق، ولا طريقة سهلة لمعرفة أيّها فعليًا
حصل بمجرّد قراءة الكود:
Animal* a = &d;
Dog* bad = (Dog*)a; // ❌ أي عامل استُخدم فعليًا؟ غير واضح
Dog* good = static_cast<Dog*>(a); // ✅ النيّة صريحة، وسهل البحث عنها بالكود
أخطاء شائعة
- استخدام
static_castللتنزيل (base → derived) دون التأكّد فعليًا من نوع الكائن — لا يوجد تحقّق، فالخطأ يظهر كسلوك غريب لاحقًا وليس كخطأ فوري. - استخدام
dynamic_castعلى صنف أساس بدون أي دالةvirtual— خطأ ترجمة، لأن التحقّق وقت التشغيل يحتاج جدول الدوال الافتراضية (vtable). - تجاهل فحص نتيجة
dynamic_castعلى مؤشّر (قد تكونnullptr) ثم استخدامها مباشرة — انهيار فوري. - اللجوء لـ
reinterpret_castلحل مشكلة نوع بدل إعادة تصميم الكود — إشارة شبه أكيدة أن هناك خطأ تصميم أعمق.
🎯 التالي: الربط المُهيكل وstd::tuple — التعامل مع قيم متعددة بأناقة.