Upcasting و Downcasting
به فرآیندی که میتوانیم یک شیء (اشاره گر یا مرجع) از کلاس فرزند را به عنوان یک شیء از کلاس پدر در نظر بگیریم، Upcasting گفته میشود؛ یا به عبارت دیگر اگر اشاره گر کلاس پایه حاوی آدرس شیء ایجاد شده از کلاس مشتق باشد، به این فرایند Upcasting گفته می شود. برای انجام Upcasting نیاز نیست کار خاصی انجام دهید. اگر یک شیء از کلاس فرزند را داخل یک شیء از کلاس پدر بریزید این کار انجام میشود:
1: #include <iostream> 2: #include <string> 3: using namespace std; 4: 5: class Parent 6: { 7: public: 8: void sleep() 9: { 10: cout << "Father is sleeping" << endl; 11: } 12: }; 13: 14: class Child : public Parent 15: { 16: public: 17: void gotoSchool() 18: { 19: cout << "The child goes to school" << endl; 20: } 21: }; 22: 23: int main() 24: { 25: Parent *parent; 26: Child child; 27: 28: parent = &child; 29: 30: parent->sleep(); 31: }
Father is sleeping.
کلاس Child فرزند کلاس Parent است، بنابراین همه اعضای دادهای و توابع عضو کلاس Parent را به ارث میبرد. در نتیجه هر کاری که ما با یک شیء از کلاس Parent میتوانیم انجام دهیم با یک شیء از کلاس Child نیز میتوانیم انجام دهیم. پس تابعی که برای کار با یک اشاره گر (مرجع) از Parent ایجاد شده میتواند همان اعمال را بر روی یک شیء از Child نیز بدون هیچ مشکلی انجام دهد. خطوط 28-25 کد بالا را به دو صورت زیر هم می توان نوشت:
Parent *parent = new Child();
یا
Child child; Parent *parent = &child;
به فرآیندی که ما یک اشاره گر (مرجع) از کلاس پدر را به یک اشاره گر (مرجع) از کلاس فرزند تبدیل میکنیم، Downcasting گفته میشود. برای انجام این کار باید حتماً از تبدیل صریح (cating) استفاده کرد. دلیل این محدودیت این است که در بیشتر موارد رابطه is-a متقارن نیست. برای مثال خرگوش یک حیوان است ولی هر حیوانی خرگوش نیست و این به معنی عدم تقارن در رابطه is-a است. اعضای دادهای و توابع عضو جدیدی که در کلاس فرزند وجود دارد، نمیتواند به کلاس پدر اضافه شود:
Parent *parent; Child *child = (Child*)&parent; child->gotoSchool();
ما نمیتوانیم از طریق یک شیء از کلاس Parent به تابع ()gotoSchool دسترسی داشته باشیم.
Child *child = &parent; // actually this won't compile // error: cannot convert from 'Parent *' to 'Child *'
کد بالا کامپایل نمیشود زیرا نمیتواند به صورت ضمنی یک اشاره گر از Parent را به یک اشاره گر از Child تبدیل کند. میتوانیم از اشاره گر برای فراخوانی تابع ()gotoSchool به صورت زیر استفاده کنیم:
child -> gotoSchool();
از آنجایی که Parent یک Child نیست (یک Parent به تابع ()gotoSchool نیازی ندارد)، بنابراین عمل downcasting در خط بالا میتواند یک عملیات نا امن (unsafe) باشد. در ++C نوع خاصی از تبدیل صریح به نام dynamic_cast یا تبدیل صریح پویا پشتیبانی میشود که این تبدیل را انجام میدهد. بنا بر قوانین شیء گرایی، اشیاء یک کلاس فرزند را همیشه میتوانیم در داخل متغیرهایی از کلاس پدر بریزیم. اما Dwoncasting بر خلاف این قوانین است. زمانی که یک اشاره گر به یک کلاس پدر ایجاد میکنیم دو امکان وجود دارد، یا این اشاره گر به یک شیء از همان کلاس پدر اشاره میکند یا اینکه با عمل upcasting به یک شیء از کلاس فرزند اشاره میکند.
dynamic cast عملگری است که میتواند یک نوع را به صورت امن، به یک نوع دیگر تبدیل کند. اگر عمل تبدیل به صورت امن ممکن بود، آدرس شیء تبدیل شده را بر میگرداند. در غیر این صورت null pointer یا همان مقدار 0 را بر میگرداند. به مثال زیر توجه کنید:
1: #include <iostream> 2: #include <string> 3: using namespace std; 4: 5: class Parent 6: { 7: public: 8: void sleep() 9: { 10: cout << "Father is sleeping" << endl; 11: } 12: }; 13: 14: class Child : public Parent 15: { 16: public: 17: void gotoSchool() 18: { 19: cout << "The child goes to school" << endl; 20: } 21: }; 22: 23: int main() 24: { 25: Parent *parent = new Parent; 26: Parent *child = new Child; 27: 28: Child *p1 = (Child *)parent; // #1 29: Parent *p2 = (Child *)child; // #2 30: }
در دو خط 28 و 29 مثال بالا ما عمل تبدیل صریح را انجام دادیم. سوالی که در اینجا مطرح میشود این است که کدام تبدیل امن است؟ از بین دو حالتی که در کد بالا وجود دارد، فقط یک حالت است که تضمین میکند این تبدیل به صورت امن تبدیل شود. تبدیل صریح 1# امن نیست، زیرا آدرس یک شیء از کلاس پدر (Parent) را در داخل یک اشاره گر از کلاس فرزند (Child) قرار دادیم. با توجه به این حالت انتظار داریم که با استفاده از یک شیء از کلاس پدر بتوانیم تابع ()gotoSchool را فراخوانی کنیم، در صورتی که نمیتوانیم از طریق یک شیء از کلاس پدر به اعضای کلاس فرزند دسترسی داشته باشیم. اما تبدیل صریح 2# امن است، زیرا میتوانیم آدرس یک شیء از کاس فرزند را در داخل یک اشاره گر از نوع کلاس پدر قرار دهیم. نحوه استفاده از عملگر dynamic_cast را به صورت کلی در زیر مشاهده میکنید:
Child *child = dynamic_cast<Child *>(parent)
عملگر dynamic_cast بررسی میکند که آیا میتواند اشاره گر parent را به صورت صریح و امن به نوع Child * تبدیل کند یا خیر؟ اگر بتواند، آدرس شیء و در غیر اینصورت 0 را بر میگرداند. ما چگونه میتوانیم از dynamic_cast استفاده کینم؟
void f(Parent* parent) { Child *child = dynamic_cast<Child*>(parent); if(child) { // we can safely use child } }
در کد بالا اگر (child) از نوع Child باشد یا به صورت مستقیم و یا غیر مستقیم فرزند کلاس Child باشد، dynamic_cast اشاره گر parent رابه یک اشاره گر از نوع Child تبدیل میکند. در غیر این صورت اگر نتواند این تبدیل را انجام دهد مقدار 0 که همان null pointer است را بر میگرداند. به عبارت دیگر میتوانیم قبل از اینکه عملیاتی را بر روی اشاره گر parent انجام دهیم، با استفاده از dynamic_cast بررسی کنیم که آیا این تبدیل را میتوان انجام داد یا خیر. به صورت کلی ما زمانی به dynamic_cast نیاز داریم که بخواهیم عملیاتی را بر روی شیئی از کلاس فرزند انجام دهیم ولی فقط یک اشاره گر یا مرجع به کلاس پدر را در اختیار داریم. Upcasting و Downcasting در مبحث چند ریختی کاربرد دارند که در درس آینده توضیح می دهیم.