(أو كتابة تمويه ، سلوك غامض والمحاذاة ، يا إلهي!)مرحبًا بالجميع ، في غضون أسابيع قليلة ، نطلق سلسلة جديدة في الدورة التدريبية
"C ++ Developer" . سيتم تخصيص هذا الحدث لموادنا الحالية.
ما هو التعرج صارمة؟ أولاً ، نصف ماهية الاسم المستعار ، ثم نكتشف ما هو الصرامة.
في C و C ++ ، يرتبط التعرج بنوع التعبيرات المسموح لنا بالوصول إليها إلى القيم المخزنة. في كل من C و C ++ ، يعرّف المعيار أي تعبيرات تسمية صالحة لأنواع. يُسمح للمترجم والمحسن أن نفترض أننا نتبع بدقة قواعد الاسم المستعار ، ومن هنا يأتي مصطلح - قاعدة الاسم المستعار الصارم (قاعدة الاسم المستعار الصارمة). إذا حاولنا الوصول إلى قيمة باستخدام نوع غير صالح ، يتم تصنيفه على أنه سلوك غير محدد (UB). عندما يكون لدينا سلوك غير مؤكد ، يتم إجراء كل الرهانات ، فإن نتائج برنامجنا تتوقف عن الاعتماد عليها.
لسوء الحظ ، في حالة حدوث انتهاكات مستعارة صارمة ، فإننا نحصل غالبًا على النتائج المتوقعة ، مما يترك احتمالية أن الإصدار المستقبلي من المحول البرمجي مع التحسين الجديد ينتهك الشفرة التي اعتبرناها صالحة. هذا أمر غير مرغوب فيه ، ومن المفيد فهم القواعد الصارمة للتعرج وتجنب كسرها.

من أجل فهم أفضل للسبب الذي يجعلنا قلقين بشأن هذا الأمر ، سنناقش المشكلات التي تنشأ عند انتهاك قواعد الاسم المستعار الصارمة ، اكتب العقوبة ، حيث يتم استخدامها غالبًا في قواعد التعرجات الصارمة ، وكذلك كيفية إنشاء التورية بشكل صحيح ، جنبًا إلى جنب مع بعض المساعدة الممكنة مع C ++ 20 لتبسيط التورية وتقليل فرصة حدوث أخطاء. سنقوم بتلخيص المناقشة من خلال النظر في بعض الطرق للكشف عن انتهاكات قواعد التعرجات الصارمة.
أمثلة أوليةدعونا نلقي نظرة على بعض الأمثلة ، ومن ثم يمكننا مناقشة ما هو مذكور بالضبط في المعايير (المعايير) ، والنظر في بعض الأمثلة الإضافية ، ومن ثم معرفة كيفية تجنب التعرجات الصارمة وتحديد الانتهاكات التي فاتناها. هنا
مثال لا ينبغي أن يفاجئك:
int x = 10; int *ip = &x; std::cout << *ip << "\n"; *ip = 12; std::cout << x << "\n";
لدينا int * الإشارة إلى الذاكرة التي تشغلها int ، وهذا هو التعرج صالح. يجب أن يفترض المُحسِّن أن التعيينات عبر ip يمكنها تحديث القيمة التي يشغلها x.
يوضح
المثال التالي الاسم المستعار ، مما يؤدي إلى سلوك غير محدد:
int foo( float *f, int *i ) { *i = 1; *f = 0.f; return *i; } int main() { int x = 0; std::cout << x << "\n"; // Expect 0 x = foo(reinterpret_cast<float*>(&x), &x); std::cout << x << "\n"; // Expect 0? }
في وظيفة foo ، نأخذ int * و float *. في هذا المثال ، ندعو foo ونضع المعلمتين للإشارة إلى نفس موقع الذاكرة ، والذي يحتوي في هذا المثال على int. لاحظ أن
reinterpret_cast تخبر المترجم بمعالجة التعبير كما لو كان يحتوي على النوع المحدد بواسطة معلمة القالب. في هذه الحالة ، نطلب منه معالجة تعبير & x كما لو كان من النوع float *. يمكننا أن نتوقع بسذاجة أن تكون نتيجة cout الثانية هي 0 ، ولكن عندما يتم تمكين التحسين باستخدام -O2 و gcc ، ستحصل clang على النتيجة التالية:
0
1
قد يكون هذا غير متوقع ، ولكنه صحيح تمامًا ، لأننا تسببنا في سلوك غير محدد. لا يمكن أن يكون Float اسمًا مستعارًا صالحًا لكائن int. لذلك ، يمكن للمحسن أن يفترض أن الثابت 1 المخزن أثناء إلغاء التسجيل i سيكون هو القيمة المرجعة ، لأن الحفظ خلال f لا يمكن أن يؤثر بشكل صحيح على الكائن int. يوضح توصيل الشفرة في Compiler Explorer أن هذا هو ما يحدث بالضبط (
مثال ):
foo(float*, int*):
يفترض المحسن الذي يستخدم تحليل الاسم المستعار المبني على النوع (TBAA) أنه سيتم إرجاع 1 ، وينقل القيمة الثابتة مباشرةً إلى سجل eax ، الذي يخزن قيمة الإرجاع. يستخدم TBAA قواعد اللغة المتعلقة بالأنواع المسموح بها للتعجيل لتحسين التحميل والتخزين. في هذه الحالة ، يعرف TBAA أن التعويم لا يمكن أن يكون اسمًا مستعارًا لـ int ، ويحسن عملية التحميل حتى الموت.
الآن إلى المرجعماذا يقول المعيار بالضبط حول ما يُسمح لنا ولا يُسمح له بالقيام به؟ اللغة القياسية ليست واضحة ، لذلك بالنسبة لكل عنصر سأحاول تقديم أمثلة على الكود توضح المعنى.
ماذا يقول معيار C11؟
يوضح المعيار C11 ما يلي في قسم "6.5 تعبيرات" في الفقرة 7:
يجب أن يكون للكائن القيمة المخزنة الخاصة به ، والتي يتم الوصول إليها فقط باستخدام تعبير lvalue ، الذي يحتوي على أحد الأنواع التالية: 88) - نوع متوافق مع النوع الفعال للكائن ،
int x = 1; int *p = &x; printf("%d\n", *p); //* p lvalue- int, int
- نسخة مؤهلة من نوع متوافق مع نوع الكائن الحالي ،
int x = 1; const int *p = &x; printf("%d\n", *p); // * p lvalue- const int, int
- نوع يمثل علامة أو بدون علامة تقابل نوعًا مؤهلاً من الكائنات ،
int x = 1; unsigned int *p = (unsigned int*)&x; printf("%u\n", *p ); // *p lvalue- unsigned int,
انظر الحاشية 12 للحصول على ملحق gcc / clang ، والذي يسمح لك بتعيين int * int * غير الموقعة ، حتى لو لم تكن أنواع متوافقة.
- نوع يمثل علامة مع أو بدون علامة تقابل إصدارًا مؤهلاً من النوع الحالي للكائن ،
int x = 1; const unsigned int *p = (const unsigned int*)&x; printf("%u\n", *p ); // *p lvalue- const unsigned int, ,
- نوع كلي أو مدمج يتضمن أحد الأنواع المذكورة أعلاه بين أعضائه (بما في ذلك ، بشكل متكرر ، عضو في جمعية مجمعة أو مضمنة) ، أو
struct foo { int x; }; void foobar( struct foo *fp, int *ip );// struct foo - , int , *ip // foo f; foobar( &f, &f.x );
- نوع الشخصية.
int x = 65; char *p = (char *)&x; printf("%c\n", *p ); // * p lvalue- char, . // - .
ما C ++ 17 مشروع معيار يقولينص معيار مشروع C ++ 17 في المقطع 11 [basic.lval]: إذا حاول البرنامج الوصول إلى قيمة مخزنة لكائن من خلال glvalue بخلاف أحد الأنواع التالية ، فإن السلوك غير معرف: 63 (11.1) هو نوع ديناميكي من الكائنات ،
void *p = malloc( sizeof(int) ); // , int *ip = new (p) int{0}; // placement new int std::cout << *ip << "\n"; // * ip glvalue- int,
(11.2) - نسخة مؤهلة من السيرة الذاتية (السيرة الذاتية - const ومتقلبة) من النوع الحيوي للكائن ،
int x = 1; const int *cip = &x; std::cout << *cip << "\n"; // * cip glvalue const int, cv- x
(11.3) - نوع مشابه (كما هو محدد في 7.5) للنوع الديناميكي للكائن ،
//
(11.4) - نوع يمثل علامة أو بدون علامة تقابل النوع الديناميكي لكائن ما ،
// si ui ,
// godbolt (https://godbolt.org/g/KowGXB) , .
signed int foo( signed int &si, unsigned int &ui ) { si = 1; ui = 2; return si; }
(11.5) - هو نوع يحمل علامة أو بدون علامة ، يتوافق مع الإصدار المؤهل من السيرة الذاتية للنوع الديناميكي للكائن ،
signed int foo( const signed int &si1, int &si2); // ,
(11.6) - نوع تجميعي أو مدمج يتضمن أحد الأنواع أعلاه بين عناصره أو عناصر بيانات غير ثابتة (بما في ذلك ، بشكل متكرر ، عنصر أو عنصر بيانات غير ثابت لجمعية فرعية أو تحتوي على ارتباطات) ،
struct foo { int x; };
// Compiler Explorer (https://godbolt.org/g/z2wJTC)
int foobar( foo &fp, int &ip ) { fp.x = 1; ip = 2; return fp.x; } foo f; foobar( f, fx );
(11.7) - هو نوع (ربما مؤهل بـ cv) نوع فئة أساسية لنوع كائن ديناميكي ،
struct foo { int x ; }; struct bar : public foo {}; int foobar( foo &f, bar &b ) { fx = 1; bx = 2; return fx; }
(11.8) - اكتب char أو char غير موقعة أو std :: byte.
int foo( std::byte &b, uint32_t &ui ) { b = static_cast<std::byte>('a'); ui = 0xFFFFFFFF; return std::to_integer<int>( b ); // b glvalue- std::byte, uint32_t }
تجدر الإشارة إلى أن
signed char
مدرجة في القائمة أعلاه ، وهذا هو الفرق الملحوظ من C ، الذي يتحدث عن نوع الحرف.
اختلافات خفيةوبالتالي ، على الرغم من أننا نرى أن C و C ++ يتحدثان عن أشياء مماثلة حول التعرج ، هناك بعض الاختلافات التي يجب أن نكون على دراية بها. لا يحتوي C ++ على مفهوم C لنوع
صالح أو
متوافق ، ولا يحتوي C على مفهوم C ++ لنوع
ديناميكي أو مماثل. على الرغم من أن كلاهما لهما تعبيرات lvalue و rvalue ، فإن C ++ له أيضًا تعبيرات glvalue و prvalue و xvalue. هذه الاختلافات تقع خارج نطاق هذه المقالة إلى حد كبير ، ولكن أحد الأمثلة المثيرة للاهتمام هو كيفية إنشاء كائن من الذاكرة المستخدمة بواسطة malloc. في C ، يمكننا تعيين نوع صالح ، على سبيل المثال ، الكتابة إلى الذاكرة عبر lvalue أو memcpy.
// C, C ++ void *p = malloc(sizeof(float)); float f = 1.0f; memcpy( p, &f, sizeof(float)); // *p - float C // float *fp = p; *fp = 1.0f; // *p - float C
لا تكفي أي من هذه الطرق في C ++ ، مما يتطلب وضع الجديد:
float *fp = new (p) float{1.0f} ; // *p float
هل أنواع int8_t و uint8_t char؟من الناحية النظرية ، لا ينبغي أن يكون int8_t أو uint8_t من أنواع char ، ولكن في الممارسة العملية يتم تنفيذها بهذه الطريقة. هذا أمر مهم لأنه إذا كان حقًا أنواع أحرف ، فسيكون أيضًا أسماء مستعارة مثل أنواع الأحرف. إذا لم تكن على علم بذلك ، فقد
يؤدي ذلك إلى تدهور غير متوقع في الأداء . نرى أن
glibc typedef
int8_t
و
uint8_t
signed char
و
unsigned char
على التوالي.
سيكون هذا صعب التغيير ، لأنه بالنسبة لـ C ++ سيكون هناك فجوة في ABI. سيؤدي ذلك إلى تغيير تشويه الاسم وتقسيم أي واجهة برمجة تطبيقات باستخدام أي من هذه الأنواع في واجهتها.
نهاية الجزء الاول. وسوف نتحدث عن لعبة الكتابة والمحاذاة في غضون أيام قليلة.
اكتب تعليقاتك ولا تفوت
الندوة المفتوحة على شبكة الإنترنت ، والتي ستعقد يوم 6 مارس من قبل رئيس قسم تطوير التكنولوجيا في Rambler & Co ،
ديمتري شيبوردايف .