C ++: جلسة علم الآثار العفوي ولماذا لا يجب استخدام الوظائف المتغيرة بأسلوب C

بدأ كل شيء ، كالمعتاد ، بخطأ. هذه هي المرة الأولى التي عملت فيها مع Java Native Interface وفي الجزء C ++ ، قمت بلف دالة تنشئ كائن Java. هذه الوظيفة - CallVoidMethod - متغيرة ، أي بالإضافة إلى مؤشر لبيئة JNI ، ومؤشر لنوع الكائن المراد إنشاؤه ، ومعرف للطريقة المطلوبة (في هذه الحالة ، المنشئ) ، فإنه يأخذ عددًا عشوائيًا من الحجج الأخرى. وهو أمر منطقي ، لأنه يتم تمرير هذه الوسيطات الأخرى إلى الطريقة التي تم استدعاؤها على جانب Java ، ويمكن أن تكون الأساليب مختلفة ، مع عدد مختلف من الوسيطات من أي نوع.

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

في ساعتين تمكنت من تجربة عدة إصدارات من JVM ، من الثامن إلى الحادي عشر ، لأنه: أولاً ، هذه هي تجربتي الأولى مع JVM ، وفي هذا الصدد ، وثقت في StackOverflow أكثر من نفسي ، وثانيًا ، شخص ثم في StackOverflow ، نصحت في هذه الحالة بعدم استخدام OpenJDK ، ولكن OracleJDK وليس 8 ، ولكن 10. وعندها فقط لاحظت أخيرًا أنه بالإضافة إلى المتغير CallVoidMethod هناك CallVoidMethodV ، والذي يأخذ عددًا عشوائيًا من الحجج عبر va_list .

ما لم يعجبني أكثر في هذه القصة هو أنني لم ألاحظ على الفور الفرق بين الحذف (علامة الحذف) و va_list . وبعد أن لاحظت ، لم أستطع أن أشرح لنفسي ما هو الفرق الأساسي. لذا ، نحتاج إلى التعامل مع علامات الحذف ، ومع va_list ، و (بما أننا ما زلنا نتحدث عن C ++) مع قوالب متغيرة.

ماذا عن القطع الناقص و va_list في المعيار


يصف معيار C ++ فقط الاختلافات بين متطلباته ومتطلبات المعيار C. سيتم مناقشة الاختلافات نفسها لاحقًا ، ولكن الآن سأشرح بإيجاز ما يقوله المعيار C (بدءًا من C89).

  • يمكنك إعلان وظيفة تأخذ عددًا عشوائيًا من الحجج. أي يمكن أن تحتوي الدالة على وسيطات أكثر من المعلمات. للقيام بذلك ، يجب أن تنتهي قائمة معلماته بحذف ، ولكن يجب أيضًا وجود معلمة ثابتة واحدة على الأقل [C11 6.9.1 / 8] :

     void foo(int parm1, int parm2, ...); 
  • لا يتم تمرير معلومات حول عدد وأنواع الحجج المطابقة للحذف إلى الوظيفة نفسها. أي بعد آخر معلمة مسماة ( parm2 في المثال أعلاه) [C11 6.7.6.3/9] .
  • للوصول إلى هذه الوسيطات ، يجب عليك استخدام نوع va_list المعلن في رأس <stdarg.h> ووحدات الماكرو 4 (3 قبل معيار C11): va_start و va_arg و va_end و va_copy (بدءًا من C11) [C11 7.16] .

    على سبيل المثال
     int add(int count, ...) { int result = 0; va_list args; va_start(args, count); for (int i = 0; i < count; ++i) { result += va_arg(args, int); } va_end(args); return result; } 

    نعم ، لا تعرف الوظيفة عدد الحجج التي لديها. انها بحاجة لتمرير هذا الرقم بطريقة أو بأخرى. في هذه الحالة ، من خلال وسيطة واحدة مسماة (خيار شائع آخر هو تمرير NULL كوسيطة أخيرة ، كما في execl أو 0).
  • لا يمكن أن تحتوي الوسيطة المسماة الأخيرة على فئة تخزين register ؛ ولا يمكن أن تكون دالة أو صفيفًا. خلاف ذلك ، سلوك غير محدد [C11 7.16.1.4/4] .
  • علاوة على ذلك ، بالنسبة إلى الحجة الأخيرة التي تم تسميتها وجميع الحجاج المجهولين ، يتم تطبيق " تعزيز الحجة الافتراضية " ( تعزيز الحجة الافتراضية ؛ إذا كانت هناك ترجمة جيدة لهذا المفهوم إلى اللغة الروسية ، فأنا سعيد باستخدامه). هذا يعني أنه إذا كانت الوسيطة تحتوي على char ، أو short (مع أو بدون علامة) أو float ، فيجب الوصول إلى المعلمات المقابلة على أنها int أو int (مع أو بدون علامة) أو double . خلاف ذلك ، سلوك غير محدد [C11 7.16.1.1/2] .
  • حول النوع va_list يقال فقط أنه تم التصريح عنه في <stdarg.h> وهو مكتمل (أي أن حجم كائن من هذا النوع معروف) [C11 7.16 / 3] .

لماذا؟ لكن لأن!


لا توجد أنواع كثيرة في C. لماذا تم إعلان va_list في المعيار ، ولكن لم يتم ذكر أي شيء عن هيكله الداخلي؟

لماذا نحتاج إلى علامة حذف إذا كان يمكن تمرير عدد عشوائي من الحجج للدالة عبر va_list ؟ يمكن القول الآن: "كسكر نحوي" ، لكني متأكد منذ 40 عامًا ، أنه لم يكن هناك وقت للسكر.

فيليب جيمس بلاوجر يقول فيليب جيمس بلاوغر في كتاب The Standard C library - 1992 - أنه تم إنشاء C في البداية حصريًا لأجهزة الكمبيوتر PDP-11. وهناك كان من الممكن فرز جميع الحجج للدالة باستخدام حساب المؤشر البسيط. ظهرت المشكلة مع شعبية C ونقل المترجم إلى بنى أخرى. الطبعة الأولى من لغة البرمجة C بقلم بريان كيرنيغان ودينيس ريتشي - 1978 - تنص صراحة على ما يلي:
بالمناسبة ، لا توجد طريقة مقبولة لكتابة دالة محمولة لعدد تعسفي من الحجج ، لأنه لا توجد طريقة محمولة للدالة المطلوبة لمعرفة عدد الوسائط التي تم تمريرها إليها عند استدعاؤها. ... printf ، أكثر وظائف لغة C شيوعًا لعدد عشوائي من الحجج ، ... ليست محمولة ويجب تنفيذها لكل نظام.
يصف هذا الكتاب printf ، ولكن ليس به vprintf ، ولا يذكر النوع ووحدات الماكرو va_* . تظهر في الإصدار الثاني من لغة البرمجة C (1988) ، وهذا هو الفضل في لجنة التطوير للمعيار C الأول (C89 ، المعروف أيضًا باسم ANSI C). أضافت اللجنة العنوان <stdarg.h> إلى المعيار ، كأساس <varargs.h> أنشأه أندرو كونيج لزيادة قابلية تشغيل نظام UNIX OS. تقرر ترك وحدات الماكرو va_* أنها وحدات ماكرو بحيث يكون من السهل على المترجمين الحاليين دعم المعيار الجديد.

الآن ، مع ظهور C89 وعائلة va_* ، أصبح من الممكن إنشاء وظائف متغيرة محمولة. وعلى الرغم من أن البنية الداخلية لهذه العائلة لا تزال غير موصوفة بأي شكل من الأشكال ، ولا توجد متطلبات لذلك ، فمن الواضح بالفعل لماذا.

بدافع الفضول المطلق ، يمكنك العثور على أمثلة لتطبيق <stdarg.h> . على سبيل المثال ، توفر نفس "مكتبة C القياسية" مثالًا لـ Borland Turbo C ++ :

<stdarg.h> من بورلاند Turbo C ++
 #ifndef _STADARG #define _STADARG #define _AUPBND 1 #define _ADNBND 1 typedef char* va_list #define va_arg(ap, T) \ (*(T*)(((ap) += _Bnd(T, _AUPBND)) - _Bnd(T, _ADNBND))) #define va_end(ap) \ (void)0 #define va_start(ap, A) \ (void)((ap) = (char*)&(A) + _Bnd(A, _AUPBND)) #define _Bnd(X, bnd) \ (sizeof(X) + (bnd) & ~(bnd)) #endif 


يستخدم SystemV ABI الأحدث بكثير لـ AMD64 هذا النوع لـ va_list :

va_list من SystemV ABI AMD64
 typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1]; 


بشكل عام ، يمكننا أن نقول أن النوع ووحدات الماكرو va_* توفر واجهة قياسية لاجتياز الحجج الخاصة بدالة متغيرة ، ويعتمد تنفيذها لأسباب تاريخية على المترجم والمنصات المستهدفة والهندسة المعمارية. علاوة على ذلك ، ظهر علامة حذف (أي دالات متغيرة بشكل عام) في C قبل va_list (أي الرأس <stdarg.h> ). ولم يتم إنشاء va_list ليحل محل علامات الحذف ، ولكن لتمكين المطورين من كتابة وظائفهم المتغيرة المحمولة.

يحافظ C ++ إلى حد كبير على التوافق مع الإصدارات السابقة مع C ، لذلك ينطبق كل ما سبق عليها. ولكن هناك أيضًا ميزات.

دالات متغيرة في C ++


شاركت مجموعة عمل WG21 في تطوير معيار C ++. في عام 1989 ، تم أخذ معيار C89 الذي تم إنشاؤه حديثًا كأساس ، والذي تغير تدريجيًا لوصف C ++ نفسه. في عام 1995 ، تم استلام الاقتراح N0695 من جون ميكو ، حيث اقترح المؤلف تغيير القيود على وحدات الماكرو va_* :

  • لأن تسمح لك C ++ على عكس C بالحصول على عنوان register المتغيرات ، ثم يمكن أن تحتوي الوسيطة المسماة الأخيرة لدالة متغيرة على فئة التخزين هذه.
  • لأن تنتهك الروابط التي ظهرت في C ++ القاعدة غير المكتوبة لوظائف المتغير C - يجب أن يتطابق حجم المعلمة مع حجم نوعها المعلن - ثم لا يمكن أن تكون الوسيطة المسماة الأخيرة ارتباطًا. خلاف ذلك ، سلوك غامض.
  • لأن في لغة C ++ لا يوجد مفهوم " رفع نوع الوسيطة افتراضيًا " ، ثم العبارة
    إذا تم تعريف المعلمة parmN بـ ... نوع غير متوافق مع النوع الذي ينتج بعد تطبيق الترقيات الافتراضية للوسيطة ، فإن السلوك غير محدد
    يجب استبداله بـ
    إذا تم تعريف المعلمة parmN بـ ... نوع غير متوافق مع النوع الذي ينتج عند تمرير وسيطة لا يوجد لها معلمة ، فإن السلوك غير محدد
لم أقم حتى بترجمة النقطة الأخيرة من أجل مشاركة الألم. أولاً ، يبقى " تصعيد نوع الوسيطة الافتراضي " في معيار C ++ [C ++ 17 8.2.2 / 9] . وثانيًا ، كنت محتارًا لفترة طويلة حول معنى هذه العبارة ، مقارنة بالمعيار C ، حيث كل شيء واضح. فقط بعد قراءة N0695 فهمت أخيراً: أعني نفس الشيء.

ومع ذلك ، تم اعتماد جميع التغييرات 3 [C ++ 98 18.7 / 3] . بالعودة إلى C ++ ، اختفى شرط وجود وظيفة متغيرة واحدة على الأقل من المعلمات المسماة (في هذه الحالة لا يمكنك الوصول إلى الآخرين ، ولكن المزيد عن ذلك لاحقًا) ، وتم استكمال قائمة الأنواع الصالحة من الوسيطات غير المسماة بمؤشرات لأعضاء الفئة وأنواع POD .

لم يجلب معيار C ++ 03 أي تغييرات في الوظائف المتغيرة. بدأ C ++ 11 في تحويل وسيطة غير مسماة من النوع std::nullptr_t إلى void* وسمح std::nullptr_t المسموح لهم ، حسب تقديرهم ، بدعم الأنواع ذات std::nullptr_t [C ++ 11 5.2.2 / 7] . سمحت C ++ 14 باستخدام الدوال والمصفوفات كآخر معلمة مسماة [C ++ 14 18.10 / 3] ، وحظر C ++ 17 استخدام توسيع حزمة المعلمات ( توسيع الحزمة ) والمتغيرات التي تم التقاطها بواسطة لامدا [C ++ 17 21.10.1 / 1] .

ونتيجة لذلك ، أضافت C ++ وظائف متنوعة إلى مخاطرها. فقط دعم النوع غير المحدد مع المنشئين / المدمرات غير التافهة يستحق ذلك. أدناه سأحاول تقليل جميع الميزات غير الواضحة للوظائف المتغيرة في قائمة واحدة واستكمالها بأمثلة محددة.

كيفية استخدام الوظائف المتغيرة بسهولة وبشكل غير صحيح


  1. من غير الصحيح الإعلان عن آخر وسيطة مسماة بنوع مروج ، أي char أو char signed char unsigned char singed short أو unsigned short أو float . ستكون النتيجة وفقًا للمعيار هي سلوك غير محدد.

    رمز غير صالح
     void foo(float n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 


    من بين جميع المترجمات التي كنت في متناول اليد (gcc ، clang ، MSVC) ، أصدرت clang فقط تحذيرًا.

    تحذير عصبي
     ./test.cpp:7:18: warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^ 

    وعلى الرغم من أن الشفرة المترجمة تصرفت بشكل صحيح في جميع الحالات ، فلا يجب الاعتماد عليها.

    سيكون على حق
     void foo(double n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  2. من غير الصحيح إعلان آخر وسيطة مسماة كمرجع. أي رابط. يعد المعيار في هذه الحالة أيضًا بسلوك غير محدد.

    رمز غير صالح
     void foo(int& n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

    قام مجلس التعاون الخليجي 7.3.0 بتجميع هذا الرمز دون تعليق واحد. أصدر lang 6.0.0 تحذيرًا ، لكنه لا يزال يجمعه .

    تحذير عصبي
     ./test.cpp:7:18: warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^ 

    في كلتا الحالتين ، عمل البرنامج بشكل صحيح (محظوظ ، لا يمكنك الاعتماد عليه). لكن MSVC 19.15.26730 ميزت نفسها - رفضت ترجمة الشفرة ، لأن va_start ألا تكون وسيطة va_start مرجعًا.

    خطأ من MSVC
     c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\vadefs.h(151): error C2338: va_start argument must not have reference type and must not be parenthesized 

    حسنًا ، يبدو الخيار الصحيح ، على سبيل المثال ، مثل هذا
     void foo(int* n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  3. من الخطأ طلب va_arg لرفع النوع - char أو short أو float .

    رمز غير صالح
     #include <cstdarg> #include <iostream> void foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, float) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); } int main() { foo(0, 1, 2.0f, 3); return 0; } 

    إنه أكثر إثارة للاهتمام هنا. يعطي gcc at compilation تحذيرًا بأنه من الضروري استخدام double بدلاً من float ، وإذا استمر تنفيذ هذا الرمز ، فسينتهي البرنامج بخطأ.

    تحذير مجلس التعاون الخليجي
     ./test.cpp:9:15: warning: 'float' is promoted to 'double' when passed through '...' std::cout << va_arg(va, float) << std::endl; ^~~~~~ ./test.cpp:9:15: note: (so you should pass 'double' not 'float' to 'va_arg') ./test.cpp:9:15: note: if this code is reached, the program will abort 

    في الواقع ، تعطل البرنامج مع شكوى حول تعليمات غير صالحة.
    يظهر تحليل تفريغ أن البرنامج تلقى إشارة SIGILL. ويظهر أيضا بنية va_list . لمدة 32 بت هذا

     va = 0xfffc6918 "" 

    أي va_list هو مجرد char* . لـ 64 بت:

     va = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7ffef147e7e0, reg_save_area = 0x7ffef147e720}} 

    أي بالضبط ما هو موصوف في SystemV ABI AMD64.

    clang at compilation يحذر من سلوك غير محدد ويقترح أيضًا استبدال float double .

    تحذير عصبي
     ./test.cpp:9:26: warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' [-Wvarargs] std::cout << va_arg(va, float) << std::endl; ^~~~~ 

    لكن البرنامج لم يعد يتعطل ، ينتج الإصدار 32 بت:

     1 0 1073741824 

    64 بت:

     1 0 3 

    تنتج MSVC نفس النتائج بالضبط ، فقط دون سابق إنذار ، حتى مع /Wall .

    هنا يمكن الافتراض أن الفرق بين 32 و 64 بت يرجع إلى حقيقة أنه في الحالة الأولى ، يقوم ABI بتمرير جميع الوسيطات من خلال المكدس إلى الوظيفة المطلوبة ، وفي الثانية ، الوسيطات الأربعة الأولى (Windows) أو الستة (Linux) من خلال المعالج ، والباقي من خلال كومة [ ويكي ]. ولكن لا ، إذا اتصلت بـ foo ليس بـ 4 وسائط ، ولكن بـ 19 وسيطرت عليها بنفس الطريقة ، فستكون النتيجة هي نفسها: فوضى كاملة في الإصدار 32 بت ، والأصفار لجميع float في الإصدار 64 بت. أي النقطة هي بالطبع في ABI ، ولكن ليس في استخدام السجلات لتمرير الحجج.

    حسنًا ، بالطبع ، للقيام بذلك
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, double) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  4. من غير الصحيح أن يتم تمرير مثيل لفئة مع مُنشئ غير بديهي أو مُدمّر كوسيطة غير مسماة. ما لم يكن بالطبع مصير هذا الرمز يثيرك على الأقل أكثر من "تجميع وتشغيل هنا والآن."

    رمز غير صالح
     #include <cstdarg> #include <iostream> struct Bar { Bar() { std::cout << "Bar default ctor" << std::endl; } Bar(const Bar&) { std::cout << "Bar copy ctor" << std::endl; } ~Bar() { std::cout << "Bar dtor" << std::endl; } }; struct Cafe { Cafe() { std::cout << "Cafe default ctor" << std::endl; } Cafe(const Cafe&) { std::cout << "Cafe copy ctor" << std::endl; } ~Cafe() { std::cout << "Cafe dtor" << std::endl; } }; void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto b = va_arg(va, Bar); va_end(va); } int main() { Bar b; Cafe c; foo(1, b, c); return 0; } 

    Clang هي الأكثر صرامة على الإطلاق. يرفض ببساطة ترجمة هذا الرمز لأن الوسيطة الثانية ، va_arg ليست من نوع POD ، ويحذر من أن البرنامج سوف va_arg عند بدء التشغيل.

    تحذير عصبي
     ./test.cpp:23:31: error: second argument to 'va_arg' is of non-POD type 'Bar' [-Wnon-pod-varargs] const auto b = va_arg(va, Bar); ^~~ ./test.cpp:31:12: error: cannot pass object of non-trivial type 'Bar' through variadic function; call will abort at runtime [-Wnon-pod-varargs] foo(1, b, c); ^ 

    لذلك سيكون ، إذا كنت لا تزال -Wno-non-pod-varargs مع علامة -Wno-non-pod-varargs .

    يحذر MSVC من أن استخدام أنواع مع منشئات غير تافهة في هذه الحالة ليس قابلاً للنقل.

    تحذير من MSVC
     d:\my documents\visual studio 2017\projects\test\test\main.cpp(31): warning C4840:    "Bar"          

    لكن الشفرة تترجم وتعمل بشكل صحيح. يتم الحصول على ما يلي في وحدة التحكم:

    نتيجة الإطلاق
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    أي يتم إنشاء نسخة فقط في وقت استدعاء va_arg ، والحجة ، كما اتضح ، يتم تمريرها عن طريق المرجع. بطريقة ما ليس واضحًا ، لكن المعيار يسمح بذلك.

    يجمع gcc 6.3.0 دون تعليق واحد. الإخراج هو نفسه:

    نتيجة الإطلاق
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    لا يحذر gcc 7.3.0 من أي شيء ، لكن السلوك يتغير:

    نتيجة الإطلاق
     Bar default ctor Cafe default ctor Cafe copy ctor Bar copy ctor Before va_arg Bar copy ctor Bar dtor Bar dtor Cafe dtor Cafe dtor Bar dtor 

    أي هذا الإصدار من المترجم يمرر الوسيطات حسب القيمة ، وعندما يتم استدعاؤه ، يقوم va_arg بعمل نسخة أخرى. سيكون من الممتع البحث عن هذا الاختلاف عند التبديل من الإصدار السادس إلى الإصدار السابع من دول مجلس التعاون الخليجي إذا كان للمنشئين / المدمرين آثارًا جانبية.

    بالمناسبة ، إذا مررت بشكل صريح وطلبت إشارة إلى الفصل:

    رمز خاطئ آخر
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto& b = va_arg(va, Bar&); va_end(va); } int main() { Bar b; Cafe c; foo(1, std::ref(b), c); return 0; } 

    ثم كل المترجمين سيلقون خطأ. كما هو مطلوب من قبل المعيار.

    بشكل عام ، إذا كنت تريد ذلك حقًا ، فمن الأفضل تمرير الحجج بالمؤشر.

    مثل هذا
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto* b = va_arg(va, Bar*); va_end(va); } int main() { Bar b; Cafe c; foo(1, &b, &c); return 0; } 


الدقة الزائدة والوظائف المتغيرة


من ناحية ، كل شيء بسيط: المطابقة مع علامة الحذف هي أسوأ من المطابقة مع وسيطة مسماة عادية ، حتى في حالة تحويل النوع القياسي أو المحدد من قبل المستخدم.

مثال الزائد
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo(int) { std::cout << "Ordinary function" << std::endl; } int main() { foo(1); foo(1ul); foo(); return 0; } 


نتيجة الإطلاق
 $ ./test Ordinary function Ordinary function C variadic function 

ولكن هذا لا يعمل إلا إلى أن الدعوة إلى foo بدون حجج يجب النظر فيها بشكل منفصل.

استدعاء فو دون الحجج
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } int main() { foo(1); foo(); return 0; } 

إخراج المترجم
 ./test.cpp:16:9: error: call of overloaded 'foo()' is ambiguous foo(); ^ ./test.cpp:3:6: note: candidate: void foo(...) void foo(...) ^~~ ./test.cpp:8:6: note: candidate: void foo() void foo() ^~~ 

كل شيء وفقًا للمعيار: لا توجد حجج - لا توجد مقارنة مع القطع الناقص ، وعندما يتم حل الحمل الزائد ، تصبح الوظيفة المتغيرة أسوأ من الوظيفة المعتادة.

متى يستحق مع ذلك استخدام وظائف متغيرة


حسنًا ، لا تتصرف الوظائف المتغيرة في بعض الأحيان بشكل واضح للغاية ، وفي سياق C ++ يمكن أن تتحول بسهولة إلى سهولة التنقل. هناك العديد من النصائح على الإنترنت مثل "لا تنشئ وظائف C متغيرة أو تستخدمها" ، لكنها لن تزيل دعمها من معيار C ++. لذلك هناك بعض الفوائد لهذه الميزات؟ حسنا هناك.

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

    مثال
     template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static void detect(const U&); static int detect(...); public: static constexpr bool value = std::is_same<void, decltype(detect(std::declval<T>()))>::value; }; 

    على الرغم من أنه في C ++ 14 يمكنك القيام بشكل مختلف قليلاً.

    مثال آخر
     template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static constexpr bool detect(const U*) { return true; } template <class U> static constexpr bool detect(...) { return false; } public: static constexpr bool value = detect<T>(nullptr); }; 

    وفي هذه الحالة ، من الضروري بالفعل مشاهدة ما يمكن للوسائط detect(...) . أفضل تغيير خطين واستخدام بديل حديث للوظائف المتغيرة ، خالية من جميع أوجه القصور فيها.

قوالب متغيرة أو كيفية إنشاء وظائف من عدد عشوائي من الحجج في C ++ الحديثة


تم اقتراح فكرة القوالب المتغيرة من قبل دوجلاس جريجور وجاككو يارفي وغاري باول مرة أخرى في عام 2004 ، أي قبل 7 سنوات من اعتماد معيار C ++ 11 ، حيث تم دعم هذه القوالب المتغيرة رسميًا.تضمن المعيار مراجعة ثالثة لاقتراحهم ، N2080 .

منذ البداية ، تم إنشاء قوالب متغيرة بحيث أتيحت للمبرمجين الفرصة لإنشاء وظائف آمنة من النوع (والمحمولة!) من عدد عشوائي من الحجج. هدف آخر هو تبسيط الدعم لقوالب الصف مع عدد متغير من المعلمات ، لكننا نتحدث الآن فقط عن الوظائف المتغيرة.

جلبت القوالب المتغيرة ثلاثة مفاهيم جديدة إلى C ++ [C ++ 17 17.5.3] :

  • المعلمات القالب الحزمة ( قالب حزمة المعلمة ) - هو قالب المعلمة، بدلا من التي كان من الممكن تحويل أي (بما في ذلك 0) عدد حجة القالب.
  • حزمة من معلمات الدوال ( حزمة معلمات الدوال ) - تبعاً لذلك ، هذه معلمة دالة تأخذ أي عدد (بما في ذلك 0) من وسيطات الدوال ؛
  • وتوسيع الحزمة ( توسيع الحزمة ) هو الشيء الوحيد الذي يمكن القيام به مع حزمة المعلمات.

مثال
 template <class ... Args> void foo(const std::string& format, Args ... args) { printf(format.c_str(), args...); } 

class ... Args — , Args ... args — , args... — .

توجد قائمة كاملة بمكان وكيفية توسيع حزم المعلمات في المعيار نفسه [C ++ 17 17.5.3 / 4] . وفي سياق مناقشة الوظائف المتغيرة ، يكفي القول:

  • يمكن توسيع حزمة معلمة الدالة في قائمة وسيطات دالة أخرى
     template <class ... Args> void bar(const std::string& format, Args ... args) { foo<Args...>(format.c_str(), args...); } 

  • أو إلى قائمة التهيئة
     template <class ... Args> void foo(const std::string& format, Args ... args) { const auto list = {args...}; } 

  • أو إلى قائمة التقاط لامدا
     template <class ... Args> void foo(const std::string& format, Args ... args) { auto lambda = [&format, args...] () { printf(format.c_str(), args...); }; lambda(); } 

  • يمكن توسيع حزمة أخرى من معلمات الدالة في تعبير الالتفاف
     template <class ... Args> int foo(Args ... args) { return (0 + ... + args); } 

    ظهرت التعابير في C ++ 14 ويمكن أن تكون أحادية وثنائية ، يمينًا ويسارًا. الوصف الأكثر اكتمالاً ، كما هو الحال دائمًا ، موجود في المعيار [C ++ 17 8.1.6] .
  • يمكن توسيع كلا نوعي حزم المعلمات إلى عامل sizeof ...
     template <class ... Args> void foo(Args ... args) { const auto size1 = sizeof...(Args); const auto size2 = sizeof...(args); } 


في الكشف عن حزمة القطع واضحة وهناك حاجة لدعم مختلف القوالب ( أنماط ) عن ولتجنب هذا الغموض.

على سبيل المثال
 template <class ... Args> void foo() { using OneTuple = std::tuple<std::tuple<Args>...>; using NestTuple = std::tuple<std::tuple<Args...>>; } 

OneTuple — ( std:tuple<std::tuple<int>>, std::tuple<double>> ), NestTuple — , — ( std::tuple<std::tuple<int, double>> ).

مثال على تنفيذ printf باستخدام قوالب متغيرة


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

printf على القوالب
 void printf(const char* s) { while (*s) { if (*s == '%' && *++s != '%') throw std::runtime_error("invalid format string: missing arguments"); std::cout << *s++; } } template <typename T, typename ... Args> void printf(const char* s, T value, Args ... args) { while (*s) { if (*s == '%' && *++s != '%') { std::cout << value; return printf(++s, args...); } std::cout << *s++; } throw std::runtime_error("extra arguments provided to printf"); } 

أظن ، ثم ظهر هذا النمط من تعداد الحجج المتغيرة - من خلال استدعاء متكرر للوظائف المحملة. ولكن ما زلت أفضل الخيار دون التكرار.

printf على القوالب ودون العودية
 template <typename ... Args> void printf(const std::string& fmt, const Args& ... args) { size_t fmtIndex = 0; size_t placeHolders = 0; auto printFmt = [&fmt, &fmtIndex, &placeHolders]() { for (; fmtIndex < fmt.size(); ++fmtIndex) { if (fmt[fmtIndex] != '%') std::cout << fmt[fmtIndex]; else if (++fmtIndex < fmt.size()) { if (fmt[fmtIndex] == '%') std::cout << '%'; else { ++fmtIndex; ++placeHolders; break; } } } }; ((printFmt(), std::cout << args), ..., (printFmt())); if (placeHolders < sizeof...(args)) throw std::runtime_error("extra arguments provided to printf"); if (placeHolders > sizeof...(args)) throw std::runtime_error("invalid format string: missing arguments"); } 

الدقة الزائدة ووظائف القالب المتغير


عند الحل ، تعتبر هذه الوظائف المتنوعة ، بعد أخرى ، بأنها قياسية وأقل تخصصًا. ولكن لا توجد مشكلة في حالة مكالمة بدون حجج.

مثال الزائد
 #include <iostream> void foo(int) { std::cout << "Ordinary function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } template <class T> void foo(T) { std::cout << "Template function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); foo(2.0); foo(1, 2); return 0; } 

نتيجة الإطلاق
 $ ./test Ordinary function Ordinary function without arguments Template function Template variadic function 

عندما يتم حل الحمل الزائد ، لا يمكن أن تتجاوز وظيفة القالب المتغير سوى دالة C المتغيرة (على الرغم من سبب مزجها؟). باستثناء - بالطبع! - استدعاء بدون حجج.

استدعاء بدون حجج
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); return 0; } 

نتيجة الإطلاق
 $ ./test Template variadic function C variadic function 

هناك مقارنة مع القطع الناقص - تفقد الوظيفة المقابلة ، ولا توجد مقارنة مع القطع الناقص - ووظيفة القالب أقل شأنا من غير القالب.

ملاحظة سريعة حول سرعة وظائف القالب المتغير


في عام 2008 ، قدم Loïc Joly اقتراحه N2772 إلى لجنة التوحيد القياسي C ++ ، حيث أظهر عمليًا أن وظائف القالب المتغير تعمل بشكل أبطأ من الوظائف المماثلة ، والتي تكون حجة قائمة التهيئة ( std::initializer_list). ورغم أن هذا كان مخالفا لإثبات النظرية للمؤلف نفسه، اقترح جولي لتنفيذ std::min، std::maxو std::minmaxذلك من خلال القوائم التهيئة بدلا من أنماط البديل.

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

ومع ذلك، في C ++ 11 والمعايير لاحقة std::min، std::maxو std::minmax- وهذا هو وظائف القالب المعتاد، عدد التعسفي من الحجج التي تنتقل عن طريق قائمة التهيئة.

موجز وخلاصة


لذلك ، وظائف متغيرة على غرار C:

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

الاستخدام الوحيد المسموح به للوظائف المتغيرة هو التفاعل مع C API في كود C ++. لكل شيء آخر ، بما في ذلك SFINAE ، هناك وظائف قالب متغيرة:

  • اعرف عدد وأنواع كل حججهم.
  • اكتب بأمان ، لا تغير أنواع حججهم.
  • هم يدعمون تمرير الحجج بأي شكل - بالقيمة ، بالمؤشر ، بالإشارة ، بالارتباط العالمي.
  • مثل أي دالة C ++ أخرى ، لا توجد قيود على أنواع الوسائط.
  • ( C ), .

يمكن أن تكون وظائف القالب المتغير أكثر تطويلًا مقارنة بنظيراتها على غرار C وأحيانًا تتطلب إصدارًا غير محمّل من القالب (اجتياز الوسيطة العودية). فهم أصعب في القراءة والكتابة. ولكن كل هذا يدفع أكثر من ذلك بسبب عدم وجود أوجه القصور المدرجة ووجود المزايا المدرجة.

حسنًا ، الاستنتاج بسيط: تبقى الوظائف المتنوعة في نمط C في C ++ فقط بسبب التوافق مع الإصدارات السابقة ، وهي توفر مجموعة واسعة من الخيارات لتصويب ساقك. في لغة C ++ الحديثة ، يُنصح بشدة بعدم كتابة وظائف جديدة ، وإن أمكن ، عدم استخدام وظائف C المتغيرة الحالية. تنتمي وظائف القالب المتغير إلى عالم C ++ الحديث وهي أكثر أمانًا. استخدمهم.

الأدب والمصادر



ملاحظة


من السهل العثور على نسخ إلكترونية من الكتب المذكورة على الإنترنت وتنزيلها. لكنني لست متأكدًا من أنها ستكون قانونية ، لذلك لا أقدم روابط.

Source: https://habr.com/ru/post/ar430064/


All Articles