بدأ كل شيء ، كالمعتاد ، بخطأ. هذه هي المرة الأولى التي عملت فيها مع
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).
لماذا؟ لكن لأن!
لا توجد أنواع كثيرة في 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 ++ وظائف متنوعة إلى مخاطرها. فقط دعم النوع غير المحدد مع المنشئين / المدمرات غير التافهة يستحق ذلك. أدناه سأحاول تقليل جميع الميزات غير الواضحة للوظائف المتغيرة في قائمة واحدة واستكمالها بأمثلة محددة.
كيفية استخدام الوظائف المتغيرة بسهولة وبشكل غير صحيح
- من غير الصحيح الإعلان عن آخر وسيطة مسماة بنوع مروج ، أي
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); }
- من غير الصحيح إعلان آخر وسيطة مسماة كمرجع. أي رابط. يعد المعيار في هذه الحالة أيضًا بسلوك غير محدد.
رمز غير صالح 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); }
- من الخطأ طلب
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); }
- من غير الصحيح أن يتم تمرير مثيل لفئة مع مُنشئ غير بديهي أو مُدمّر كوسيطة غير مسماة. ما لم يكن بالطبع مصير هذا الرمز يثيرك على الأقل أكثر من "تجميع وتشغيل هنا والآن."
رمز غير صالح #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 ++ الحديث وهي أكثر أمانًا. استخدمهم.الأدب والمصادر
ملاحظة
من السهل العثور على نسخ إلكترونية من الكتب المذكورة على الإنترنت وتنزيلها. لكنني لست متأكدًا من أنها ستكون قانونية ، لذلك لا أقدم روابط.