النطاقات Scopes
المتغيرات العامة Global variables
إذا أردت التصريح عن نسخة واحدة من متغير ما، وكانت هذه النسخة متاحة للوصول في عدة ملفات مصدرية source files، فمن الممكن أن نجعلها في النطاق العام global scope باستخدام الكلمة المفتاحية extern
، فهي تخبر المصرِّف بوجود تعريف لهذا المتغير في مكان ما داخل الشيفرة الحالية، لذا فمن الممكن استخدامه في أي مكان، كما ستُجرى جميع عمليات القراءة / الكتابة في نفس المكان من الذاكرة.
يتألّف المثال التالي من عدّة ملفات مصدرية:
الملف my_globals.h:
#ifndef __MY_GLOBALS_H__ #define __MY_GLOBALS_H__ extern int circle_radius; // سيُعرَّف في مكان ما circle_radius تخطر المصرّف بأنّ #endif
الملف foo1.cpp:
#include "my_globals.h" int circle_radius = 123; // extern تعريف المتغير الخارجي
الملف main.cpp:
#include "my_globals.h" #include <iostream> int main() { std::cout << "The radius is: " << circle_radius << "\n"; ' return 0; }
سينتج الخرج التالي:
The radius is: 123
المتغيرات المضمنة Inline variables
يُسمح بتعريف مُتغير مُضمّن Inline variable في عدّة وحدات ترجمة دون انتهاك قاعدة التعريف الواحد. وفي حال تعريف متغيّر مُضمّن أكثر من مرة، فسيَدمِج الرابط linker كلّ تَعاريفه في كائن واحد في البرنامج النهائي.
يمكن تعريف حقل ساكن static data member في صنف إن صُرِّح عنه بأنّه مُضمّن inline
. على سبيل المثال، يمكن تعريف الصنف في المثال التالي في الترويسة.
ملاحظة: قبل C++ 17، كان من الضروري توفير ملفّ .cpp
لاحتواء تعريف Foo::num_instances
كي نضمن ألا يُعرّف أكثر من مرّة واحدة، لكن في C++ 17، أصبحت مُختلف تعريفات المتغيّر المضمن Foo::num_instances
تشير إلى نفس الكائن int
.
// not thread-safe قد لا يكون متوافقا مع المسالك class Foo { public: Foo() { ++num_instances; }~Foo() { --num_instances; } inline static int num_instances = 0; };
وفي حالّة خاصة، تكون الحقول الساكنة للتعابير الثابتة constexpr
مضُمّنة بطريقة غير صريحة implicitly.
class MyString { public: MyString() { /* ... */ } // ... static constexpr int max_size = INT_MAX / 2; };
صار التعريف constexpr int MyString::max_size
ضروريًا في كل وحدات الترجمة في إصدار C++ 1.
نطاق كتلة بسيط
يبدأ نطاق متغيّر ما في كتلة { ... }
بعد التصريح وينتهي عند نهاية الكتلة، فإذا كانت هناك كتلة متشعّبة nested block، فتستطيع الكتلة الداخلية إخفاء نطاق المتغيّر الذي صُرِّح عنه في الكتلة الخارجية.
{ int x = 100; // ^ // يبدأ هنا `x` نطاق // } // <- ينتهي هنا `x` نطاق
في حال التصريح عن متغير جديد داخل الكتلة المُتشعِّبة، وكان ذلك المتغير يحمل اسم متغير موجود في الكتلة الخارجية، فإنه يخفي المتغير الأول الذي في الكتلة الخارجية.
{ int x = 100; { int x = 200; std::cout << x; // → 200 } std::cout << x; // → 100 }
قاعدة التعريف الواحد ODR
انتهاك قاعدة ODR عبر تحليل التحميل الزائد
يمكن انتهاك ODR إذا لم يشِر البحث عن الأسماء إلى نفس الكيان حتى في حال استخدام مقاطع tokens متماثلة للتعبير عن دالة مضمّنة معيّنة. لاحظ الدالة func
في المثال التالي:
- header.h
void overloaded(int); inline void func() { overloaded('*'); }
-
foo.cpp ، تشير
overloaded
إلى(void overloaded(char
:
#include "header.h" void foo() { func(); }
-
bar.cpp، تشير
overloaded
إلى(void overloaded(char
:
void overloaded(char); // أخرى “include” قد تأتي من عبارات تضمين #include "header.h" void bar() { func(); }
لدينا انتهاك هنا لقاعدة ODR، إذ يشير المقطع overloaded
إلى عدّة كيانات مختلفة في نفس وحدة الترجمة.
مضاعفة الدوال المعرفة Multiply defined function
إنّ أهم نتيجة لقاعدة التعريف الواحد هي أنّ الدوال غير المضمّنة ذات الارتباط الخارجي non-inline functions with external linkage يجب ألا تُعرَّف إلا مرّة واحدة في البرنامج، لكن يمكن التصريح عنها عدّة مرات. لذا لا ينبغي تعريف مثل هذه الدوال في الترويسات، إذ يمكن إدراج الترويسة عدّة مرات من قبل عدة وحدات ترجمة مختلفة.
-
foo.h
:
#ifndef FOO_H #define FOO_H #include <iostream> void foo() { std::cout << "foo"; } void bar(); #endif
-
foo.cpp
:
#include "foo.h" void bar() { std::cout << "bar"; }
-
main.cpp
:
#include "foo.h" int main() { foo(); bar(); }
في هذا البرنامج، عُرِّفت الدالّة الأبسط foo
في الترويسة foo.h
، والتي ضُمِّنت مرّتين: مرّة من قبل foo.cpp
ومرّة أخرى من قبل main.cpp
، ولذلك تحتوي كل وحدة ترجمة على تعريف foo
الخاص بها.
لاحظ أنّ دروع التضمين include guards في foo.h
لا تمنع حدوث ذلك، إذ أنّ الملفين foo.cpp
و main.cpp
يشتملان على foo.h
بشكل منفصل، وعلى الأرجح سينتج عن محاولة إنشاء هذا البرنامج خطأٌ في وقت الربط link-time error بسبب تعريف foo
أكثر من مرّة.
ويجب التصريح عن الدوال في الترويسات وتعريفها في ملفات .cpp
المقابلة لتجنّب مثل هذه الأخطاء، مع بعض الاستثناءات.
الدوال المضمنة Inline functions
يمكن تعريف دالّة مُضمّنة inline
في عدّة وحدات ترجمة بشرط أن تكون جميع تَعاريفها متطابقة، ويجب أن تُعرّف أيضًا في كل وحدة ترجمة تُستخدم فيها، لذا يجب تعريف الدوال المضمّنة في الترويسات، ولا توجد حاجة إلى ذكرها في ملف التنفيذ implementation file.
سيتصرّف البرنامج كما لو كان هناك تعريف واحد للدالّة.
-
foo.h
:
#ifndef FOO_H #define FOO_H #include <iostream> inline void foo() { std::cout << "foo"; } void bar(); #endif
-
foo.cpp
:
#include "foo.h" void bar() { // تعريفات أخرى أكثر تعقيدا }
-
main.cpp
:
#include "foo.h" int main() { foo(); bar(); }
في هذا المثال، عُرِّفت الدالّة foo
بصورة مُضمّنة inline في الترويسة، أمّا الدالّة الأكثر تعقيدًا bar
فهي غير مُضمنة، وقد عُرِّفت في ملف التقديم. وتحتوي كل من وحدتي الترجمة foo.cpp
و main.cpp
على تعريفات لـ foo
، وهذا لن يسبّب مشكلة لأنّ foo
مضمّنة.
تكون الدوالّ المُعرَّفة داخل تعريف صنف ما -والتي قد تكون دوالًا تابعة أو صديقة- مضمنة بشكل غير صريح، لذا يمكن تعريف الدوال التابعة للفصل داخل تعريف الصنف، رغم أنّه يمكن إدراج التعاريف في وحدات ترجمة متعددة:
// in foo.h class Foo { void bar() { std::cout << "bar"; } void baz(); }; // in foo.cpp void Foo::baz() { // التعريف }
عُرِّفت الدالّة Foo::baz
خارج السطر، وعليه تكون غير مُضمّنة، ويجب ألا تُعُرِّف في الترويسة.
البحث باسم الوسيط Argument Dependent Name Lookup
عند البحث عن الدوال، تُجمع "الأصناف المرتبطة" associated classes، و"فضاءات الأسماء المرتبطة" associated namespaces التي تحقق أحد الشروط التالية، وفقًا لنوع الوسيط T
.
عند البحث عن الدوال فإننا نجدها عبر جمع "الأصناف المرتبطة" associated classes أولًا، وكذلك "فضاءات الأسماء المرتبطة" associated namespaces، التي تحقق شرطًا أو أكثر من الشروط أدناه وفقًا لنوع الوسيط T
، لكن سنعرض قبل هذا القواعد الخاصة بأسماء الأصناف والتعداد وقوالب أسماء الأصناف:
-
إذا كان
T
صنفًا مُتشعِّبا nested class، أو عضوَ تعدادٍ member enumeration، فسيُضاف الصنف المحيطة به إلى مجموعة البحث. -
إذا كان
T
تعدادًا -قد يكون أيضًا عضوًا من صنف-، يُضاف أعمق فضاء اسم - innermost namespace - داخله إلى مجموعة البحث. -
إذا كان
T
صنفًا -قد يكون مُتشعِّبًا أيضًا-، تُضاف جميع أصنافه الأساسية، وكذلك الصنف نفسه، وأعمق فضاءات الاسم في جميع الأصناف المرتبطة. -
إذا كان
T
من النوعClassTemplate<TemplateArguments>
-وهو صنف أيضًا-، تُضاف الأصناف وفضاءات الاسم المرتبطة بوسائط نوع القالب، وفضاء الاسم الخاص بوسائط قالب القالب template template argument، والصنف المحيط بوسائط قالب القالب، إذا كان وسيط القالبِ template argument قالبَ عضوٍ member template.
هناك بعض القواعد التي تخصّ الأنواع المضمّنة أيضًا:
-
إذا كان
T
مؤشّرًا يشير إلى نوعU
، أو مصفوفةً مؤلّفة من عناصر من النوعU
، تُضاف أصناف وفضاءات الأسماء المرتبطة بـU
إلى مجموعة البحث. في المثال:void (*fptr)(A);f(fptr);
، تُضاف فضاءات الأسماء والأصناف المرتبطة بـvoid(A)
(انظر القاعدة التالية). -
إذا كان
T
نوع دالّة، تُضاف الأصناف وفضاءات الأسماء المرتبطة بأنواع المُعاملات والقيمة المعادة. في المثال:void(A)
، تُضاف فضاءات الأسماء والأصناف المرتبطة بـA
. -
إذا كان
T
مؤشّرًا يشير إلى عضو، تُضاف الأصناف وفضاءات الأسماء المرتبطة بنوع ذلك العضو (قد تنطبق على كل من مؤشّرات التوابع ومؤشّرات الحقول). في مثالB A::*p; void (A::*pf)(B); f(p);f(pf);
؛ تُضاف فضاءات الأسماء والأصناف المرتبطة بـA
وB
وvoid(B)
، والتي تطبّق القواعد أعلاه التي تخصّ أنواع الدوال.
يُعثَر على جميع الدوال والقوالب داخل جميع فضاءات الأسماء المرتبطة من خلال البحث باسم الوسيط، أو البحث المعتمد على الوسيط argument dependent lookup. ويُعثَر على الدوال الصديقة friend functions لنطاق الأسماء المُصرَّح عنها في الأصناف المرتبطة، والتي عادة ما تكون غير مرئية. بالمقابل، تُتجَاهل الموجّهات directives. وتكون جميع الاستدعاءات في المثال التالي صالحة حتى دون الحاجة إلى تأهيل f
بفضاء الاسم عند الاستدعاء.
namespace A { struct Z {}; namespace I { void g(Z); } using namespace I; struct X { struct Y {}; friend void f(Y) {} }; void f(X p) {} void f(std::shared_ptr < X > p) {} } // أمثلة على الاستدعاءات f(A::X()); f(A::X::Y()); f(std::make_shared < A::X > ()); g(A::Z()); // "using namespace I;"غير صالح، يُتجاهل
حزم المعاملات Parameter packs
قالب بحزمة معاملات template with a parameter pack
template < class...Types > struct Tuple {};
حزمة المُعاملات parameter pack هي مُعاملُ قالبٍ template parameter يقبل وسيط قالب أو أكثر، أو لا يقبل على الإطلاق، ويكون القالب متغايرًا variadic إذا كان يحتوي على حزمة مُعاملات واحدة على الأقل.
توسيع حزمة معاملات
يُوسَّع نمط parameter_pack ...
إلى قائمة من البدائل لحزمة parameter_pack
مفصولة بالفاصلة الأجنبية ,
، وذلك مع كل معامل من معاملاتها:
template<class T> // أساس التكرار void variadic_printer(T last_argument) { std::cout << last_argument; } template<class T, class ...Args> void variadic_printer(T first_argument, Args... other_arguments) { std::cout << first_argument << "\n"; variadic_printer(other_arguments...); // توسيع حزمة المعاملات }
عند استدعاء الشيفرة أعلاه مع variadic_printer(1, 2, 3, "hello");
، ستطبع:
1 2 3 hello
فواصل الأرقام Digit separators
من الصعب قراءة القيم العددية المؤلّفة من من أرقام كثيرة، فمثلًا:
- حاول نُطق 7237498123
- قارن بين 237498123 وبين 237499123 من حيث التساوي.
- حدّد العدد الأكبر من بين 237499123 و 20249472.
وقد عرَّف الإصدار C++ 14 علامةَ الاقتباس '
على أساس فاصلة للأرقام، بحيث تُستخدَم داخل الأعداد والقيم مصنَّفة النوع المُعرَّفة من قبل المستخدِم user-defined literals لتسهيل قراءة الأعداد الكبيرة. انظر:
الإصدار ≥ C++ 14
long long decn = 1'000'000'000ll; long long hexn = 0xFFFF'FFFFll; long long octn = 00'23'00ll; long long binn = 0b1010'0011ll;
تُتجَاهل علامة الاقتباس المُفردة عند تحديد القيمة. مثلًا:
-
القيم مصنَّفة النوع التالية:
1048576
و1'048'576
و0X100000
و0x10'0000
و0'004'000'000
كلها لها نفس القيمة. -
القيم مصنفة النوع التالية
1.602'176'565e-19
و1.602176565e-19
لهما نفس القيمة.
ولا توجد أهمية تُذكر لموضع علامات الاقتباس المُفردة، فمثلًا، كل التعابير التالية متكافئة:
الإصدار ≥ C++ 14
long long a1 = 123456789ll; long long a2 = 123'456'789ll; long long a3 = 12'34'56'78'9ll; long long a4 = 12345'6789ll;
كذلك يُسمح باستخدام الفاصلة في القيم مصنَّفة النوع والمُعرّفة من المستخدم:
الإصدار ≥ C++ 14
std::chrono::seconds tiempo = 1'674'456s + 5'300h;
موازنة مع إصدارات C++ المختلفة
اعلم أنه يمكن تنفيذ تكرار حلقي على حاوية التسلسل c
في لغة ++C باستخدام الفهارس على النحو التالي:
for (size_t i = 0; i < c.size(); ++i) c[i] = 0;
هذه الصيغة -رغم بساطتها- عرضة لكثير من الأخطاء الشائعة، مثل استخدام عامل مقارنة خاطئ، أو متغيّر فهرسة غير صحيح:
for(size_t i = 0; i <= c.size(); ++j) c[i] = 0; ^~~~~~~~~~~~~~^
كذلك يمكن التكرار على الحاويات باستخدام المُكرّرات iterators، مع عيوب مماثلة:
for(iterator it = c.begin(); it != c.end(); ++it) (*it) = 0;
وقد قدّمت C++ 11 حلقة for
النطاقية، إضافة إلى الكلمة المفتاحية auto
، واللّتان سمحتا بكتابة شيفرات مثل:
for(auto& x : c) x = 0;
وهنا، يكون المُعاملان الوحيدان هما الحاوية c
، ومتغيّر x
لتخزين القيمة الحالية. هذا يمنع الأخطاء المشار إليها سابقا. ووفقًا لمعيار C++ 11، فإنّ التنفيذ الأساسي underlying implementation يكافئ:
for(auto begin = c.begin(), end = c.end(); begin != end; ++begin) { // ... }
في مثل هذا التنفيذ فإن التعبير auto begin = c.begin(), end = c.end();
يجبر begin
و end
على أن يكونا من نفس النوع، بينما لا تُزاد قيمة end
ولا تحصَّل dereferenced، ومن ثم فإنّ حلقة for
النطاقية لا تعمل إلا على الحاويات المُعرَّفة بواسطة زوج مُكرّر/ مُكرّر iterator/iterator. وقد خفّف معيارُ C++ 17 هذا القيد عن طريق تعديل التنفيذ:
auto begin = c.begin(); auto end = c.end(); for(; begin != end; ++begin) { // ... }
هنا يُسمح بأن تكون begin
و end
من نوعين مختلفين طالما أنه يمكن مقارنتهما للتحقّق من أنّهما غير متساويين. هذا يسمح بالتكرار على العديد من الحاويات مثل الحاويات المعرّفة عبر زوج مُكرّر/حارس iterator/sentinel.
هذا الدرس جزء من سلسلة دروس عن C++.
ترجمة -بتصرّف- للفصول
- Chapter 64: Inline variables
- Chapter 117: Scopes
- Chapter 120: One Definition Rule (ODR)
- Chapter 122: Argument Dependent Name Lookup
- Chapter 129: Parameter packs
- Chapter 135: Digit separators
- Chapter 137: Side by Side Comparisons of classic C++ examples solved via C++ vs C++11 vs C++14 vs C++1
من كتاب C++ Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.