هل يجب استخدام الدالة std:: أو مؤشر وظيفة في C++؟




function c++11 (4)

عند تنفيذ وظيفة رد اتصال في C ++ ، هل ما زلت أستخدم مؤشر الدالة C-style:

void (*callbackFunc)(int);

أو ينبغي أن أستفيد من وظيفة std :: function:

std::function< void(int) > callbackFunc;

استخدم std::function لتخزين الكائنات الاستدعاء التعسفي. يسمح للمستخدم بتوفير أي سياق مطلوب لاستدعاء؛ مؤشر وظيفة عادي لا.

إذا كنت بحاجة إلى استخدام مؤشرات دالة بسيطة لسبب ما (ربما لأنك تريد واجهة برمجة تطبيقات متوافقة مع C) ، فيجب عليك إضافة وسيطة void * user_context ، على الأقل يكون ذلك ممكنًا (وإن كان غير مريح) للوصول إلى حالة غير مباشرة. مرت على الوظيفة.


السبب الوحيد لتجنّب std::function هو دعم المترجمين التقليديين الذين يفتقرون إلى دعم هذا القالب ، والذي تم تقديمه في C ++ 11.

إذا كان دعم لغة ما قبل C ++ 11 ليس متطلبًا ، فإن استخدام std::function يمنح المتصلين المزيد من الخيارات في تنفيذ معاودة الاتصال ، مما يجعله خيارًا أفضل مقارنةً بمؤشرات الدالة "العادية". فهو يوفر لمستخدمي API مزيدًا من الخيارات ، بينما يستخرجون تفاصيل تنفيذهم للشفرة التي تقوم بإجراء الاستدعاء.


void (*callbackFunc)(int); قد تكون وظيفة رد اتصال بأسلوب C ، ولكنها وظيفة غير صالحة للاستخدام بشكل مريع.

يبدو أن الاستدعاء C ذو التصميم الجيد مثل void (*callbackFunc)(void*, int); - يحتوي على void* للسماح للشفرة التي تقوم بإعادة الاتصال للحفاظ على حالة ما بعد الدالة. عدم القيام بذلك يجبر المتصل على تخزين الولاية على مستوى العالم ، وهو أمر غير مهذب.

std::function< int(int) > ينتهي به الأمر إلى أن يكون أغلى قليلاً من int(*)(void*, int) invokation في معظم التطبيقات. ومع ذلك ، من الصعب بالنسبة لبعض المترجمين المضمنة. هناك عمليات استنساخ std::function التي تتنافس فيها دالة الاستدعاء الخاصة بمؤشر الدالة (انظر "أسرع المندوبين المحتملين" إلخ) والتي قد تشق طريقها إلى المكتبات.

الآن ، يحتاج عملاء نظام الاتصال عادةً إلى إعداد الموارد والتخلص منها عند إنشاء الاستدعاء وإزالته ، ولكي يكون على علم بعمر معاودة الاتصال. void(*callback)(void*, int) لا يوفر هذا.

في بعض الأحيان يكون ذلك متاحًا من خلال بنية الكود (فترة الاسترداد محدودة العمر) أو من خلال آليات أخرى (إلغاء تسجيلات الاتصال وما شابه).

توفر std::function وسيلة للإدارة المحدودة مدى الحياة (تنتهي آخر نسخة من الكائن عندما يتم نسيانه).

بشكل عام ، std::function ما لم تتجلى مشكلات الأداء. إذا فعلوا ذلك ، فسأبحث أولاً عن التغييرات الهيكلية (بدلاً من معاودة الاتصال لكل بكسل ، وماذا عن توليد معالج تفحص يستند إلى lambda الذي تمر به ، وهو ما ينبغي أن يكون كافيًا لتقليل النفقات الإضافية للدعوات الوظيفية إلى مستويات تافهة. ). ثم ، إذا استمر ، فسأكتب delegate أساس أسرع المندوبين المحتملين ، ونرى ما إذا كانت مشكلة الأداء تختفي.

في الغالب لن استخدم سوى مؤشرات الدالة لواجهات برمجة التطبيقات القديمة ، أو لإنشاء واجهات C للتواصل بين الشفرات المجمعة المجمعة المختلفة. كما أنني استخدمتها كتفاصيل داخلية للتنفيذ عندما أقوم بتطبيق جداول القفز ونوع المسح ، وما إلى ذلك: عندما أقوم بإنتاجها واستهلاكها ، ولا أقوم بتعريضها خارجيًا لأي رمز عميل لاستخدامها ، وتقوم المؤشرات الوظيفية بكل ما أحتاجه .

لاحظ أنه يمكنك كتابة wrappers التي تقوم بإدارة std::function<int(int)> إلى رد نمط نمط int(void*,int) ، على افتراض وجود بنية تحتية لإدارة وقت الاستدعاء مناسبة. إذن ، كاختبار دخان لأي نظام لإدارة مدى الحياة من النمط C ، أتأكد من أن التفاف std::function يعمل بشكل جيد بشكل معقول.


باختصار ، استخدم std::function ما لم يكن لديك سبب لعدم القيام بذلك.

مؤشرات الدالة لها عيوب عدم القدرة على التقاط بعض السياق. لن تكون قادرًا على سبيل المثال على تمرير دالة lambda كوظيفة اتصال تستدعي بعض متغيرات السياق (ولكنها ستنجح إذا لم يتم التقاط أي منها). وبالتالي ، فإن استدعاء متغير عضو في كائن (أي غير ثابت) غير ممكن أيضًا ، نظرًا لأن الكائن ( this -المؤشر) يحتاج إلى الالتقاط. (1)

std::function (منذ C ++ 11) هي في المقام الأول لتخزين وظيفة (لا يتطلب نقلها من حولها أن يتم تخزينها). ومن ثم إذا كنت تريد تخزين معاودة الاتصال على سبيل المثال في متغير عضو ، فربما يكون هذا هو أفضل خيار لك. ولكن أيضًا إذا لم تخزنها ، فهذا يعني "خيارًا جيدًا" جيدًا ، على الرغم من أن له عيوبًا في إدخال بعض النفقات الزائدة (صغيرة جدًا) عندما يتم استدعاؤها (لذا في حالة حرجة جدًا من حيث الأداء ، قد تكون هناك مشكلة ولكن في معظم فإنه لا ينبغي). إنه "عالمي" للغاية: إذا كنت تهتم كثيرًا بالشفرة الثابتة والمقروءة وكذلك لا ترغب في التفكير في كل خيار تقوم به (أي تريد الاحتفاظ به بسيطًا) ، استخدم std::function لكل وظيفة تقوم بتمريرها حول.

فكر في خيار ثالث: إذا كنت على وشك تنفيذ دالة صغيرة تقوم بعد ذلك بالإبلاغ عن شيء ما عبر وظيفة رد الاتصال المقدمة ، ففكر في معلمة قالب ، والتي يمكن أن تكون أي كائن قابل للاستدعاء ، أي مؤشر دالة ، أو أداة موسيقية ، أو لامدا ، std::function ... ... العيب هنا هو أن وظيفتك (الخارجية) تصبح قالبًا وبالتالي يجب تنفيذها في الرأس. من ناحية أخرى ، يمكنك الحصول على ميزة أن الاتصال إلى معاودة الاتصال يمكن أن يكون مضمنًا ، حيث إن رمز العميل الخاص بك (الخارجي) يعمل "سيشاهد" المكالمة إلى معاودة الاتصال هي معلومات النوع الدقيقة المتوفرة.

مثال للإصدار الذي يحتوي على معلمة القالب (اكتب & بدلاً من && لـ pre-C ++ 11):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

كما ترون في الجدول التالي ، كل منهم لديهم مزايا وعيوب:

+-------------------+--------------+---------------+----------------+
|                   | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture       |    no(1)     |      yes      |       yes      |
| context variables |              |               |                |
+-------------------+--------------+---------------+----------------+
| no call overhead  |     yes      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be inlined    |      no      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be stored     |     yes      |      yes      |      no(2)     |
| in class member   |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be implemented|     yes      |      yes      |       no       |
| outside of header |              |               |                |
+-------------------+--------------+---------------+----------------+
| supported without |     yes      |     no(3)     |       yes      |
| C++11 standard    |              |               |                |
+-------------------+--------------+---------------+----------------+
| nicely readable   |      no      |      yes      |      (yes)     |
| (my opinion)      | (ugly type)  |               |                |
+-------------------+--------------+---------------+----------------+

(1) توجد حلول للتغلب على هذا القيد ، على سبيل المثال تمرير البيانات الإضافية كمعلمات إضافية إلى الوظيفة (الخارجية) الخاصة بك: وظيفة myFunction(..., callback, data) callback(data) . هذا هو "معاودة الاتصال مع الوسائط" C- ، وهو أمر ممكن في C ++ (وبالطريقة المستخدمة بكثرة في WIN32 API) ولكن يجب تجنبها لأن لدينا خيارات أفضل في C ++.

(2) ما لم نتحدث عن قالب الفصل الدراسي ، أي الفئة التي تقوم بتخزين الدالة بها هي قالب. ولكن هذا يعني أن نوع الوظيفة يقرر نوع العميل الذي يقوم بتخزين رد الاتصال ، والذي لا يعد خيارًا لحالات الاستخدام الفعلية.

(3) لـ Pre + C ++ 11 ، استخدم boost::function





std