الكلمات المفتاحية هي كلمات لها معنى محدّد في C++ ولا يمكن استخدامها كمُعرّفات، كذلك لا يجوز إعادة تعريف الكلمات المفتاحية باستخدام المعالج المُسبق (preprocessor) في أيّ وحدة ترجمة تتضمن ترويسة المكتبة القياسية. لكن بأي حال فإن الكلمات المفتاحية تفقد معناها المميِّز داخل السمات (attributes).
decltype
الإصدار ≥ C++ 11
تعيد الكلمة المفتاحية decltype
نوع المعامَل الخاص بها، والذي لا يُقيَّم.
-
إذا كان المعامَل
e
اسمًا بدون أي أقواس إضافية، فإنّdecltype(e)
ستكون النوع المُصرَّح للمعاملe
. انظر المثال التالي حيث تكونv
من نوع<vector<int
:
int x = 42; std::vector<decltype(x)> v(100, x);
-
إذا كان المعامَل
e
عضوًا من صنفٍ، ولم يكن محاطًا بأيّة أقواس، فإنّdecltype(e)
ستكون النوع المُصرّح للعضو الذي تم الوصول إليه. انظر المثال التالي حيث تكونy
من نوعint
رغم أنs.x
ثابتة.
struct S { int x = 42; }; const S s; decltype(s.x) y;
-
تعيد
decltype(e)
في جميع الحالات الأخرى كلًّا من نوع وصنف التعبيرe
كما يلي: -
إذا كانت
e
تعبيرًا يساريًا (lvalue) من النوعT
، فإنّdecltype(e)
ستساويT&
. -
إذا كانت
e
تعبيرًا من فئة (xvalue ) منT
، فإنّdecltype(e)
ستساويT&&
. -
إذا كانت
e
تعبيرًا من فئة (prvalue) منT
، فإنّdecltype(e)
ستساويT
.
وهذا يشمل حالة الأقواس الخارجية:
int f() { return 42; } int & g() { static int x = 42; return x; } int x = 42; decltype(f()) a = f(); decltype(g()) b = g(); decltype((x)) c = x;
لاحظ في المثال السابق أن a
من نوع int
، و b
من نوع &int
، و c
من نوع &int
كذلك لأن x
قيمة يسارية (lvalue).
الإصدار ≥ C++ 14
يستنتج الشكل الخاص decltype(auto)
نوعَ متغيّر ما من مُهيِّئه (initializer) أو نوع الإعادة لدالة من تعليمات return
الموجودة في تعريف الدالة، باستخدام قواعد الاستنتاج الخاصة بـ decltype
بدلاً من قواعد auto
. انظر المثال التالي حيث تكون y
من نوع int
، و z
من نوع const int
، وهو النوع المصرح من x
.
const int x = 123; auto y = x; decltype(auto) z = x;
const
تمثل const
مُحدِّدًا للنوع، وتنتج النسخة المؤهلة ثبوتيًا من نوع ما عند تطبيقها على ذلك النوع.
const int x = 123; x = 456; // خطأ int& r = x; // خطأ struct S { void f(); void g() const; }; const S s; s.f(); // خطأ s.g(); // OK
تجنّب تكرار الشيفرة البرمجية في التوابع الجالبة (Getter Methods)
يمكن زيادة تحميل التوابع التي لا تختلف إلا في مؤهِّل const
، وقد تحتاج أحيانًا إلى نسختين من الجالب الذي يعيد مرجعًا إلى عضو ما. ولنفرض أن Foo
صنف له تابعان يُجرِيان عمليّات متطابقة ويُعيدان مرجعًا إلى كائن من النوع Bar
، فإنه يكون على النحو التالي:
class Foo { public: Bar & GetBar( /* some arguments */ ) { /* افعل شيئا ما هنا */ return bar; } const Bar & GetBar( /* some arguments */ ) const { /* افعل شيئا ما هنا */ return bar; } // ... };
الفرق الوحيد بينهما هو أنّ أحد التابعين غير ثابت (non-const) ويُعيد مرجعًا غير ثابت كذلك يمكن استخدامه لتعديل الكائن، أما الثاني فهو ثابت ويعيد مرجعًا ثابتًا.
وقد نميل إلى استدعاء أحد التابعيْن من التابع الآخر من أجل تجنّب تكرار الشيفرة، لكن لا يمكننا استدعاء تابع غير ثابت من تابع ثابت، وإنما نستطيع استدعاء تابع ثابت من آخر غير ثابت، وسيتطلّب ذلك استخدام "const_cast" لإزالة مُؤهِّل const
. انظر الحل فيما يلي:
struct Foo { Bar & GetBar( /*arguments*/ ) { return const_cast < Bar & > (const_cast < const Foo * > (this) - > GetBar( /*arguments*/ )); } const Bar & GetBar( /*arguments*/ ) const { /* افعل شيئًا ما هنا */ return foo; } };
في الشيفرة أعلاه، استدعينا نسخة ثابتة من GetBar
من التابع المتغير GetBar
عن طريق تحويله إلى النوع الثابت const_cast<const Foo*>(this)
. وبما أننا استدعينا تابعًا ثابتًا من آخرَ متغيرٍ فإن الكائن نفسه يكون متغيرًا، ويُسمح بإهمال الثابت. انظر المثال التالي لمزيد من الشرح:
#include <iostream> class Student { public: char& GetScore(bool midterm) { return const_cast<char&>(const_cast<const Student*>(this)->GetScore(midterm)); } const char& GetScore(bool midterm) const { if (midterm) { return midtermScore; } else { return finalScore; } } private: char midtermScore; char finalScore; }; int main() { // كائن متغير Student a;
هنا نستطيع الإسناد إلى المرجع، وتُستدعى نسخة متغيرة من GetScore، انظر بقية المثال:
a.GetScore(true) = 'B'; a.GetScore(false) = 'A'; // كائن ثابت const Student b(a);
لا زلنا نستطيع استدعاء تابع GetScore
الخاص بكائن ثابت لأننا زدنا تحميل النسخة الثابتة منه -أي من GetScore-، انظر بقية المثال:
std::cout << b.GetScore(true) << b.GetScore(false) << '\n'; }
الدوال التوابع الثابتة (Const member functions)
يمكن التصريح أنّ توابع صنف ما ثابتة (const
)، وذلك سيخبر المصرّف والقارئ أنّ هذا التابع لن يعدّل الكائن، انظر:
class MyClass { private: int myInt_; public: int myInt() const { return myInt_; } void setMyInt(int myInt) { myInt_ = myInt; } };
يكون مؤشر this
في دالة التابع const
من نوع const MyClass *
وليس MyClass *
، وهذا يعني أننا لا نستطيع تغيير أي متغير عضو داخل الدالة وإلا سيعطي المصرفُ تحذيرًا، لذا لا يمكن التصريح بأن setMyInt
ثابتة.
كذلك يجب التصريح أنّ الدوال التوابع ثابتة const
متى كان ذلك ممكنًا، ذلك أن التوابع الثابتة فقط هي التي يمكن أن تُستدعى على النوع const MyClass
. أيضًا لا يمكن التصريح عن التوابع الساكنة static
أنها ثابتة const
، ذلك أنّ التابع الساكن ينتمي إلى صنف ولا يستدعى على الكائن، فلا يمكنه تعديل المتغيّرات الداخلية للكائن، وعليه يكون التصريح عن توابع static
على أنها const
حشوًا لا فائدة منه.
المتغيرات المحلية الثابتة
تبين الشيفرة التالية كيفية التصريح عن متغيرات محلية ثابتة، وكيفية استخدامها، حيث يكون a
من نوع const int
فلا يمكن تغييره، وعليه لا يمكن تعيين قيمة جديدة إلى متغير ثابت. انظر:
const int a = 15; a = 12; // خطأ، لا يمكن تعيين قيمة جديدة لمتغير ثابت a += 1; // خطأ، لا يمكن تعيين قيمة جديدة لمتغير ثابت
جمع المراجع مع المؤشّرات:
في الشيفرة التالية، لا يمكن ربط مرجع غير ثابت بمتغير ثابت كما ترى في السطر الأول، لكن في السطر الثاني يمكن ذلك بما أن c
مرجع ثابت. انظر:
int &b = a; const int &c = a;
بالمثل، لا يمكن جمع مؤشر إلى قيمة متغيرة مع متغير ثابت، لكن يمكن ذلك كما ترى في السطر الثاني بما أن e
مؤشر إلى قيمة ثابتة:
int *d = &a; const int *e = &a
يمكن ربط e
بـ *int
أو *const int
بما أنها مؤشر غير ثابت إلى قيمة ثابتة:
int f = 0; e = &f;
e
مؤشر إلى قيمة ثابتة، وذلك يعني أن القيمة التي تشير إليها لا يمكن تغييرها بتحصيل e
:
*e = 1
يمكن تغيير القيمة التالية من خلال تحصيل مؤشر إلى قيمة غير ثابتة:
int *g = &f; *g = 1;
المؤشرات الثابتة
في المثال التالي، const int* pA = & a
هو مؤشر إلى قيمة ثابتة، ولا يمكن تغيير قيمة a
، بينما int* const pB = &a
مؤشر ثابت، ويمكن تغيير قيمة a
لكن لا يمكن تغيير قيمة المؤشر نفسه. انظر:
int a = 0, b = 2; const int* pA = & a; int* const pB = &a; const int* const pC = &a; // مؤشر ثابت إلى قيمة ثابتة. // خطأ، لا يمكن التعيين إلى مرجع ثابت *pA = b; pA = &b; *pB = b; // خطأ، لا يمكن التعيين إلى مؤشر ثابت pB = &b; // خطأ، لا يمكن التعيين إلى مرجع ثابت *pC = b; // خطأ، لا يمكن التعيين إلى مؤشر ثابت pC = &b;
asm
تأخذ الكلمة المفتاحية asm
مُعامَلًا واحدًا، وينبغي أن يكون سلسلة نصّية حرفيّة، ويتغيّر معناها بحسب التنفيذ ولكنها تُمرَّر عادةً إلى مُجمِّع التنفيذ (implementation's assembler)، مع دمج خَرْج المُجمِّع في وحدة الترجمة.
التعليمة asm
هي تعريفٌ وليست تعبيرًا، لذلك قد تظهر إمّا في نطاق كتلة (block scope) أو نطاق فضاء الاسم - namespace scope - بما في ذلك النطاق العام، ولكن قد لا تظهر asm
داخل دالة تعبير ثابت constexpr
نظرًا لتعذُّر تقييد التجميع المُضمّّن (inline assembly) بواسطة قواعد لغة C++. انظر المثال التالي:
[ [noreturn] ] void halt_system() { asm("hlt"); }
char
تمثّل char
نوعًا عدديًّا صحيحًا ذا حجم كبير بقدر يسمح بتخزين أي عضو من مجموعة محارف التطبيق الأساسية، ويُمكن أن يكون مؤشَّرًا - signed - (ولديه نطاق يتراوح بين -127 و +127، مضمنة) أو غير مؤشَّر - unsigned - (ولديه نطاق يتراوح بين 0 و 255 مُضمّنة).
const char zero = '0'; const char one = zero + 1; const char newline = '\n'; std::cout << one << newline; // تطبع 1 متبوع بسطر جديد
char16_t
الإصدار ≥ C++ 11
تمثل char16_t
نوعًا عدديًا صحيحًا غير مؤشَّر له نفس حجم ومحاذاة النوع uint_least16_t
، وعليه فهو كبير بما يكفي ليحتوي قيمةً من النوع UTF-16.
const char16_t message[] = u"你好,世界\\n"; // مرحبا بالعالم بالصينية std::cout << sizeof(message)/sizeof(char16_t) << "\\n"; // 7 يطبع
char32_t
الإصدار ++ C++ 11
تمثّل char32_t
نوعًا عدديًا صحيحًا غير مؤشّر له نفس حجم ومحاذاة uint_least32_t
، وعليه فهو كبير بما يكفي لِيحتوي قيمة من النوع UTF-32.
const char32_t full_house[] = U"▯▯▯▯▯"; // محارف ليست من المستوى الأساسي متعدد اللغات // non-BMP characters std::cout << sizeof(full_house)/sizeof(char32_t) << "\\n"; // يطبع 6
int
تمثّل int
نوعًا عدديًا صحيحًا مؤشّرًا يختلف حجمه بحسب معماريّة بيئة التنفيذ، ويشمل نطاقه من -32767 إلى +32767 مُضمّنة.
int x = 2; int y = 3; int z = x + y;
يمكن دمج int
مع unsigned
و short
و long
و long long
للحصول على أنواع عددية صحيحة أخرى.
wchar_t
تمثل wchar_t
نوعًا عدديًا صحيحًا كبيرًا بما يكفي لتمثيل جميع الأحرف في مجموعة المحارف الممتدة (extended character set) المدعومة، ولا يمكن افتراض أن wchar_t
تستخدم ترميزًا معينًا مثل UTF-16 لاختلاف طريقة تقديمها، وهي تُستخدم عادةً عندما تحتاج إلى تخزين أحرف من خارج ASCII لأنّ حجمها أكبر من char
. انظر المثال التالي الذي يعرض عبارة "مرحبا بالعالم\n" بعدة لغات:
// الأمهرية const wchar_t message_ahmaric[] = L"▯▯▯ ▯▯▯ \\n"; // الصينية const wchar_t message_chinese[] = L"你好,世界\\n"; // العبرية const wchar_t message_hebrew[] = L"םלוע םולש\\n"; // الروسية const wchar_t message_russian[] = L"Привет мир\\n"; // التاميلية const wchar_t message_tamil[] = L"ஹலே◌◌ா உலகம◌்\\n";
float
تمثّل float
نوعًا من الأعداد العشرية، وهي أصغر أنواع الأعداد العشرية في C++.
float area(float radius) { const float pi = 3.14159f; return pi * radius * radius; }
double
تمثّل double
نوعًا من الأعداد العشرية، ويشمل نطاقها float، وتشير عند دمجها مع
long إلى النوع
long double الذي يتضمّن نطاقه نطاقَ
double.
double area(double radius) { const double pi = 3.141592653589793; return pi * radius * radius; }
long
تمثل long
نوعًا عدديًّا صحيحًا مؤشَّرًا بطول يماثل int
على الأقل، ويتضمن نطاقه المجال من -2147483647 إلى +2147483647 مُضمّنة (أي، من - (2 ^ 31 - 1) إلى + (2 ^ 31 - 1))، ويمكن كتابة هذا النوع أيضًا بالصيغة long int
.
const long approx_seconds_per_year = 60L*60L*24L*365L;
يشير التركيب long double
إلى نوع عددي ذي فاصلة عائمة، والذي له النطاق الأوسع مقارنة بأنواع الأعداد العشرية الأخرى.
long double area(long double radius) { const long double pi = 3.1415926535897932385L; return pi * radius * radius; }
الإصدار ≥ C++ 11
عند تكرار long
مرتين كما في long long
فإنّها تشير إلى نوع عددي صحيح مؤشَّر طوله يساوي على الأقل طوال long
، ويشمل نطاقه على الأقل المجال من -9223372036854775807 إلى +9223372036854775807 مُضمّنة (أي، من - (2 ^ 63 - 1) إلى + (2 ^ 63 - 1)).
// دعم أحجام ملفات تصل إلى 2 تيرا بايت const long long max_file_size = 2LL << 40;
short
تشير short
إلى نوع عددي صحيح مؤشَّر طوله يساوي char
على الأقل، ويتضمن نطاقه المجال من -32767 إلى +32767 مُضمّنة، ويمكن كتابة هذا النوع أيضًا هكذا short int
.
// خلال السنة الماضية short hours_worked(short days_worked) { return 8 * days_worked; }
bool
تمثّل bool نوعًا عدديًا صحيحًا يمكن أن تكون قيمته إما true
أو false
.
bool is_even(int x) { return x % 2 == 0; } const bool b = is_even(47); // false
signed
signed
هي كلمة مفتاحية تكون جزءًا من أسماء بعض الأنواع العددية الصحيحة.
-
عند استخدامها بمفردها، تعيد النوع
int
ضمنيًا، وفي هذه الحالة فإنّ الأنواعsigned
وsigned int
وint
متماثلة. -
عند دمجها مع
char
تعيد النوعsigned char
، وهو نوع مختلف عنchar
، حتى لو كانتchar
مؤشّرة، كما يحتوي نطاقsigned char
بين -127 إلى +127 مضمّنة على الأقل. -
عند دمجها مع
short
أوlong
أوlong long
، فستكون مجرّد تكرار، لأنّ هذه الأنواع مؤشَّرة سلفًا. -
لا يمكن دمج
signed
معbool
أوwchar_t
أوchar16_t
أوchar32_t
.
مثال:
signed char celsius_temperature; std::cin >> celsius_temperature; if (celsius_temperature < -35) { std::cout << "cold day, eh?\n"; }
unsigned
unsigned
هي مُحدِّد نوع يتطلب النسخة غير المؤشّرة (unsigned) من نوع عددي صحيح.
-
عند استخدامها بمفردها، فإنّها تعيد النوع
int
ضمنيًا،، وعليه يتماثل كل منunsigned
وunsigned int
في النوع. -
يختلف النوع
unsigned char
عنchar
حتى لو كان الأخير غير مؤشّر، ويمكن استخدامه لتمثيل الأعداد الصحيحة الأصغر من 255. -
يمكن أيضًا دمج
unsigned
معshort
أوlong
أوlong
long
. ولا يمكن دمجها معbool
أوwchar_t
أوchar16_t
أوchar32_t
.
مثال:
char invert_case_table[256] = { ..., 'a', 'b', 'c', ..., 'A', 'B', 'C', ... }; char invert_case(char c) { unsigned char index = c; return invert_case_table[index]; // مباشرة invert_case_table[c] إعادة // مؤشّرة char غير مناسب إن كانت }
لاحظ في المثال السابق أن إعادة [invert_case_table[c
غير مناسب إن كانت char
مؤشَّرة.
كلمات النوع المفتاحية
class
تشمل استخدامات كلمة class
المفتاحية ما يلي:
- تقديم تعريف نوع الصنف:
class foo { int x; public: int get_x();f void set_x(int new_x); };
- تقديم مُحدِّد مفصّل للنوع يشير إلى أنّ الاسم التالي هو اسم لنوع صنف (class type)، وإن كان اسم الصنف قد صُرِّح عنه سلفًا فيمكن العثور عليه حتى لو كان مخفيًا باسم آخر، أما إن لم يكن قد صُرح عنه فسيُعدّ مُصرّحًا عنه بشكل لاحق (forward-declared).
class foo; // مُحدِّد مفصّل للنوع -> تصريح لاحق class bar { public: bar(foo & f); }; void baz();
لدينا في الشيفرة التالية محدِّد مفصَّل للنوع، وتصريح لاحق آخر، لاحظ أن الصنف ودالة ()void baz
لهما نفس الاسم:
class baz; class foo { bar b;
في الشيفرة التالية، مُحدِّد نوع مفصّل يشير إلى الصنف وليس إلى الدالة التي لها نفس الاسم:
friend class baz; public: foo(); };
- تقديم نوع المعاملات في تصريح القالب.
template < class T > const T& min(const T& x, const T& y) { return b < a ? b : a; }
-
في التصريح عن معامل قالب القالب (template template parameter)، تسبق الكلمة المفتاحيّة
class
اسمَ المعامل، ونظرًا لأنّ الوسيط الخاص بمعامل قالب القالب لا يمكن أن يكون إلّا قالب صنف (class template)، فلا حاجة لاستخدامclass
هنا، لكن رغم هذا فإنّ لغة C++ تُوجِبه. لاحظ في المثال التالي كيف تُستخدمclass
الأخيرة في الصف الأول، حيث يكونU
هو معامل قالب القالب.
template < template < class T > class U > void f() { U < int > ::do_it(); U < double > ::do_it(); }
-
لاحظ أنّه يمكن الجمع بين النقطتين الثانية والثالثة في نفس التصريح، انظر المثال التالي حيث لا يُشترط أن تكون
bar
قد ظهرت من قبل:
template < class T > class foo {}; foo < class bar > x;
الإصدار ≥ C++ 11.
-
في تصريح أو تعريف نوع عددي
enum
، تصرّح أن التعداد هو تعدادٌ نطاقي (scoped enum):
enum class Format { TEXT, PDF, OTHER, }; Format f = F::TEXT;
enum
هناك عدة أدوار للكلمة المفتاحية enum
، وهي كالتالي:
- تقديم تعريف لنوع عددي.
enum Direction { UP, LEFT, DOWN, RIGHT }; Direction d = UP;
الإصدار ≥ C++ 11
قد تُلحَق enum
اختياريًا في الإصدار C++ 11 بكلمة class
المفتاحية أو struct
من أجل تعريف نوع تعداد نطاقي، ويمكن تحديد النوع الأساسي (underlying type) لكلٍّ من الأنواع العددية النطاقية وغير النطاقية عبر وضع : T
بعد اسم النوع العددي، إذ تشير T
إلى نوع عددي صحيح.
enum class Format : char { TEXT, PDF, OTHER }; Format f = Format::TEXT; enum Language : int { ENGLISH, FRENCH, OTHER };
كذلك يمكن أن تُسبق العدّادات (Enumerators) في أنواع enum
العاديّة بعامِل النطاق (scope operator). لكن رغم ذلك ستُعدّ ضمن النطاق الذي عُرِّف فيه enum
.
Language l1, l2; l1 = ENGLISH; l2 = Language::OTHER;
-
تقديم مُحدّد مفصَّل للنوع يشير إلى أنّ الاسم التالي هو اسم نوع عددي سبق التصريح عنه. (لا يمكن استخدام مُحدّد مفصَّل للنوع في تصريح لاحق لنوع عددي)، ويمكن تسمية نوع عددي بهذه الطريقة حتى لو كان يخفيه اسم آخر. انظر في المثال التالي كيف أن صيغة
Foo foo = FOO
غير صحيح لأنFoo
تشير إلى الدالة، بينماenum Foo foo = FOO
صحيحة لأنها تشير إلى النوع العددي.
enum Foo { FOO }; void Foo() {} Foo foo = FOO; enum Foo foo = FOO;
الإصدار ≥ C++ 11
-
تقديم تصريح عن نوع عددي مبهم يصرّح عن نوع عددي لكن دون تعريفه، وقد يعيد التصريح عن نوع عددي سبق التصريح عنه، ويمكن كذلك أن يُصرِّح لاحقًا (forward declaration) عن نوع عدديّ لم يسبق التصريح عنه. لكن من الناحية الأخرى، لا يمكن لنوع سبق التصريح عنه كنوع نطاقي أن يُعاد التصريح عنه كنوع عددي وتحويله إلى نوع غير نطاقي، والعكس صحيح. ويجب أن تتوافق جميع تصريحات النوع العددي على مستوى النوع الأساسي (underlying type). كذلك يجب تحديد النوع الأساسي بشكل صريح عند التصريح اللاحق عن نوع عددي غير نطاقي،ذلك أنه لا يمكن أن يُستنتج إلى حين التعرّف على قيم العدّادات. انظر المثال التالي حيث يكون النوع الأساسي هو
int
:
enum class Format; void f(Format f); enum class Format { TEXT, PDF, OTHER, }; enum Direction; // غير صالح، يجب تحديد النوع الأساسي
struct
الكلمة المفتاحية struct
مشابهة للكلمة class
، بيد أنّ هناك بعض الاختلافات، وهي:
-
في حال تعريف نوعِ صنفٍ باستخدام الكلمة المفتاحية
struct
فستكون صلاحيات الوصول الافتراضية للأصناف الأساسية (bases) والأعضاءِ عامةً أيpublic
وليسprivate
. -
لا يمكن استخدام
struct
للتصريح عن معامل نوع قالب (template type parameter) أو قالب معامِل القالب بلclass
فقط هو من يمكنه ذلك.
union
هناك عدة أدوار للكلمة المفتاحية union
، وهي كالتالي:
- تقديم تعريف نوع الاتحاد (union type) :
// POSIX مثال من union sigval { int sival_int; void *sival_ptr; };
- تقديم محدِّد مفصّل للنوع يشير إلى أنّ الاسم التالي هو اسم لنوع اتحاد، وإذا صُرِّح عن اسم الاتحاد سلفًا فيمكن العثور عليه حتى لو كان يخفيه اسم آخر، وإلا فسيكون مُصرَّحا عنه بشكل لاحق (forward-declared):
union foo; // محدِّد مفصّل للنوع -> تصريح اللاحق. class bar { public: bar(foo & f); }; void baz(); union baz; // baz() الصنف له نفس اسم الدالة union foo { long l; union baz * b; // محدّد نوع مفصل يشير إلى الصنف وليس الدالة ذات الاسم نفسه };
الكلمة المفتاحية mutable
تعابير لامدا القابلة للتغيير
يُمنع إجراء العمليات غير الثابتة non-const
على تعابير لامدا (λ) بسبب كون العامل الضمني operator()
ثابتًا (const
)، ويمكن التصريح عن تعبير (λ) كقيمة قابلة للتغير لجعل operator()
غير ثابت (non-const
)، ومن ثم يمكننا التعديل على الأعضاء. انظر المثال التالي حيث نحصل على خطأ لأن operator()
ثابت فلا يمكننا تعديل الأعضاء.
int a = 0; auto bad_counter = [a] { return a++; };
لاحظ الآن كيف يمكننا تعديل الأعضاء بعد جعل تعبير لامدا قابلًا للتغيير:
auto good_counter = [a]() mutable { return a++; // OK } good_counter(); // 0 good_counter(); // 1 good_counter(); // 2
معدِّلات الصنف المتغيرة
تُستخدَم المُعدِّلات القابلة للتغيير mutable
في هذا السياق للإشارة إلى إمكانية تعديل حقل كائن ثابت دون التأثير على حالة الكائن المرئية من الخارج، فيفضَّل استخدام كلمة mutable
المفتاحية إن كنت تريد أن تخزّن نتيجةَ عمليّات حسابية مكلّفة وطويلة بشكل مؤقتٍ (caching).
وإذا كان لديك حقل مقفول - lock data field - (مثال: std::unique_lock
)، والذي يُقفَل ويُفتَح داخل تابع ثابت، فستكون هذه الكلمة المفتاحية مفيدة في هذه الحالة كذلك.
لكن لا تستخدم هذه الكلمة لإلغاء ثباتيّة (const-ness) كائن، انظرالمثال التالي:
class pi_calculator { public: double get_pi() const { if (pi_calculated) { return pi; } else { double new_pi = 0; for (int i = 0; i < 1000000000; ++i) { // new_pi بعض الحسابات لتحسين } pi = new_pi; pi_calculated = true; return pi; } } private: mutable bool pi_calculated = false; mutable double pi = 0; };
لاحظ أنه في الشيفرة السابقة، إن كان كل من pi
و pi_calculated
لا يقبلان التعديل فسنحصل على خطأ من المصرِّف إذ لا يمكن تعديل حقل غير قابل للتغيير في تابع ثابت.
كلمات مفتاحية أخرى
void
تمثل void
نوعًا فارغًا غير مكتمل، ويستحيل لكائن أن يكون من النوع void
، ولا مصفوفات من هذا النوع ولا حتى مرجع إليه، وإنّما يُستخدم كنوع إعادة للدوال التي لا تُعيد أيّ شيء.
-
عند استخدام
void
كنوع للقيمة المعادة من دالة فإنّها تشير إلى أنّ تلك الدالة لا تُعيد أيّ قيمة، وعند استخدامها في قائمة معاملاتِ دالةٍ فإنها تدلّ على أنّ تلك الدالة لا تأخذ أيّ معاملات (مثلًا:int main()
وint main(void)
متكافئتان) وأُجيزَت هذه الصياغة للتوافق مع C حيث تصريحات الدوال لها معنى مختلف عن مثيلاتها C++. أما عند استخدامها في تصريح مؤشّر فذلك يعني أنّ ذلك المؤشّر عام (universal). -
إذا كان المؤشّر من النوع
void *
فإنّه يستطيع الإشارة إلى أيّ متغير لم يُصرّح عنه باستخدامconst
أوvolatile
، ولا يمكن تحصيل (dereferencing) مؤشّر فارغ (من النوعvoid *
) إلا إن حُوِّل إلى نوع آخر. كذلك يمكن تحويل مؤشر فارغ إلى أيّ نوع آخر من مؤشّرات البيانات. وهذه الميزة تجعل النوعvoid*
مناسبًا لأنواع معيّنة من واجهات مسح الأنواع (type-erasing interfaces) غير الآمنة (type-unsafe)، مثل السياقات العامة في الواجهات البرمجية (API) الشبيهة بـC مثلqsort
وpthread_create
. - يمكن أن يشير المؤشّر الفارغ إلى دالة، ولكن ليس إلى عضو من صنف في C++.
void vobject; // C2182 void *pv; // okay int *pint; int i; int main() { pv = &i; // C++ لكنه ضروري في C التحويل اختياري في pint = (int * ) pv;
يمكن تحويل أيّ تعبير ليكون من نوع void
، وهو ما يسمى تعبير القيمة المهملة (discarded-value expression):
static_cast<void>(std::printf("Hello, %s!\n", name)); // إهمال القيمة المعادة
قد يكون هذا مفيدًا للإشارة إلى أنّ قيمة التعبير ليست مهمّة، وأنّ التعبير يجب تقييمه لأجل آثاره الجانبية فقط.
Volatile
الكلمة المفتاحية Volatile
هي مؤهِّلُ نوع (type qualifier) يمكن استخدامه للتصريح بأنّ كائنًا ما قابلٌ لأن يُغيَّر في البرنامج من قِبل عتاد الحاسوب وتنتج النسخة المتغيرة (volatile-qualified) من نوع عند تطبيقها عليه، ويلعب التأهيل المتغير (Volatile qualification) نفس الدور الذي تلعبه const
في نظام الأنواع، لكن volatile
لا تمنع تعديل الكائنات، بل تفرض على المُصرّف معاملة عمليّات الوصول إلى هذه الكائنات كآثار جانبية.
volatile declarator ;
في المثال أدناه، إذا لم تكن memory_mapped_port
متغيّرة (Volatile) فيمكن للمصرّف تحسين الدالة بحيث لا ينفّذُ إلا الكتابة النهائية، وسيكون ذلك غير صحيح إذا كانت sizeof(int)
أكبر من 1، ويجبر التأهيل المتغير volatile
المُصرّفَ على معاملة عمليات كتابة sizeof(int)
على أنها تأثيرَات جانبية، وعليه ينفّذها جميعهًا بالترتيب.
extern volatile char memory_mapped_port; void write_to_device(int x) { const char* p = reinterpret_cast < const char * > ( &x); for (int i = 0; i < sizeof(int); i++) { memory_mapped_port = p[i]; } }
virtual
تصرح الكلمة المفتاحية virtual
عن دالة وهمية، أو عن صنف أساسي وهمي (virtual base class).
virtual [type-specifiers] member-function-declarator virtual [access-specifier] base-class-name
المعامِلات
-
type-specifiers
: تحدّد نوع القيمة المعادة من دالة التابع الوهمي. -
member-function-declarator
: تصرّح عن دالة تابع. -
access-specifier
: تحدّد مستوى الوصول إلى الصنف الأساسي: عام (public) أو محمي (protected ) أو خاص (private)، ويمكن أن تظهر قبل أو بعد الكلمة المفتاحيةvirtual
. -
base-class-name
: تعرّف نوع صنف سبق تعريفه.
مؤشر this
this
هو مؤشّر يمكن الوصول إليه فقط داخل دوال التوابع المتغيرة (non static) لنوع أو صنف أو اتحاد أو بِنية. ويشير إلى الكائن الذي استُدعِي عليه التابع، ولا تملك دوال التوابع الساكنة مؤشر this
.
this->member-identifier
المؤشّر this
ليس جزءًا من الكائن نفسه ولا يُحسب في تعليمة الحجم sizeof
للكائن، بل يُمرَّر عنوان ذلك الكائن من قبل المُصرِّف كوسيط مخفي إلى الدالة عند استدعاء دالة تابع غير ساكن على كائن ما. انظر المثال التالي:
myDate.setMonth( 3 );
يمكن تأويله بهذه الطريقة:
setMonth( &myDate, 3 );
معظم استخدامات this
ضمنيّة، ويُسمح باستخدام this
صراحة عند الرجوع إلى أعضاء الصنف رغم أنّ ذلك غير ضروري. انظر:
void Date::setMonth(int mn) { month = mn; // هذه العبارات الثلاث متكافئة this-> month = mn; ( *this).month = mn; }
يُستخدم التعبير
*this لإعادة الكائن الحالي من دالة تابع، ويُستخدم هذا المؤشّر أيضًا لمنع التنكيس أو الإشارة الذاتية ( self-reference):
if ( &Object != this) { // لا تنفّذ في حال الإشارة الذاتية.
عبارات try و throw و catch
-
تُستخدم عبارات
try
وthrow
وcatch
لمُعالجة الاعتراضات (Exceptions) في C++، -
أولًا، استخدم كتلة
try
لتضع فيها تعليمة أو أكثر من التي قد تطلق اعتراضًا. -
يشير تعبير
throw
إلى أنّ شيئًا اعتراضيًا قد حدث في كتلةtry
-غالبًا ما يكون خطأ-، ويمكنك استخدام كائن من أيّ نوع كمعامَل لـthrow
، ويُستخدم هذا الكائن لتوصيل معلومات بخصوص الخطأ. ويوصى في معظم الحالات باستخدام الصنفstd::exception
أو أحد الأصناف المشتقة منه والمُعرّفة في المكتبة القياسية، أما إذا لم تكن تلك الاستثناءات مناسبة فيمكنك اشتقاق اعتراض خاص بك منstd::exception
. -
استخدم كتلةَ
catch
واحدة أو أكثر بعد كتلةtry
لمعالجة الاعتراضات التي يمكن إطلاقها، إذ تحدّد كل كتلة من كتلcatch
نوعًا من الاستثناءات التي يمكنها معالجتها. انظر:
MyData md; try { // شيفرة قد تؤدّي إلى إطلاق اعتراض md = GetNetworkResource(); } catch (const networkIOException& e) { // شيفرة ستُنفّذ في حال إطلاق اعتراض من نوع // networkIOException // try في كتلة ... // عرض رسالة الخطأ الخاصة بالاعتراض cerr << e.what(); } catch (const myDataFormatException & e) { // شيفرة تعالج بقيّة أنواع الاعتراضات // ... cerr << e.what(); } // throw الصيغة التالية تُظهر عبارة MyData GetNetworkResource() { // ... if (IOSuccess == false) throw networkIOException("Unable to connect"); // ... if (readError) throw myDataFormatException("Format error"); // ... }
الشيفرة التي تلي try
هي الجزء المَحمِيّ من الشيفرة، ويطلق التعبير throw
اعتراضًا، أمّا كتلة التعليمات البرمجية الموضوعة بعد catch
فهي المسؤولة عن إمساك ومعالجة الاعتراض الذي أُطلق إذا توافقت الأنواع في تعبيرات throw
و catch
.
try { throw CSomeOtherException(); } catch (...) { // إمساك كل الاستثناءات // معالجة الاستثناء جزئيا، ثم إعادة إطلاق الاستثناء ليُعالِجه معالِجٌ آخر // ... throw; }
friend
تغلف الأصناف المصممة جيدًا وظائفها لتخفي تفاصيلها في نفس الوقت الذي تقدم فيه واجهة نظيفة وبسيطة وموثقة جيدًا، وذلك يسهِّل التعديل أو إعادة التصميم طالما ظلت الواجهة كما هي. لكن قد تحتاج بعض الأصناف في الحالات الأكثر تعقيدًا إلى معرفة تفاصيل تطبيق بعضها البعض، ويتيح مفهوم الأصناف والدّوال الصديقة (Friend classes and functions) لها الوصول إلى تلك التفاصيل دون المساس بتغليف المعلومات.
-
يكون من المفيد أحيانًا أن تمنح حقّ الوصول لأعضاءِ صنفٍ ما إلى دالةٍ ليست من أعضاء ذلك الصنف، أو إلى جميع الأعضاء في صنف آخر، ولا يمكن إلّا لمنفِّذ الصنف أن يصرح عن أصدقائه، ولا يمكن أن يصرح صنف أنه صديق لصنف آخر، وبالمثل لا يمكن لدالة أن تصرح عن ذلك. استخدام كلمة
friend
واسم دالة غير عضو أو اسم صنف آخر في تعريف الصنف، وذلك لمنحه حق الوصول إلى الأعضاء الخاصّين والمحميّين في ذلك الصنف، كما يمكن التصريح عن معامِل نوع كصديق في تعريف قالب ما. - إذا صرحت عن دالة صديقة لم يسبق التصريح عنها، فستُصدَّر تلك الدالة إلى النطاق المحيط غير الصنفي (enclosing nonclass scope).
class friend F friend F; class ForwardDeclared; // اسم الصنف معروف class HasFriends { friend int ForwardDeclared::IsAFriend(); // C2039 خطأ };
الدوال الصديقة (friend functions)
- الدالة الصديقة هي دالة ليست عضوًا في صنف، ولكن لها حق الوصول إلى الأعضاء المحميّين والخواصّ في ذلك الصنف، ولا تُعدّ الدوال الصديقة من أعضاء الصنف بل هي دوال خارجية طبيعية تُمنح امتيازات وصول خاصّة.
-
لا تدخل الدوال الصديقة في نطاق الصنف ولا تُستدعى باستخدام عوامل اختيار الأعضاء
.
و->
إلّا إن كانت أعضاء من صنف آخر. - يُصرَّح عن الدالّة الصديقة من قِبل الصنف الذي يمنح حق الوصول، ويمكن وضع تصريح الصداقة في أيّ مكان في تصريح الصنف، ولا يتأثّر التصريح بالكلمات المفتاحيّة الخاصّة المسؤولة عن التحكم في الوصول.
#include <iostream> using namespace std; class Point { friend void ChangePrivate(Point & ); public: Point(void): m_i(0) {} void PrintPrivate(void) { cout << m_i << endl; } private: int m_i; }; void ChangePrivate (Point &i) { i.m_i++; } int main() { Point sPoint; sPoint.PrintPrivate(); ChangePrivate(sPoint); sPoint.PrintPrivate(); // الخرج 0 1 }
تستطيع الأصناف والبُنى أن تصرح عن أيّ دالة على أنها صديقة لها، وإذا كانت دالة ما صديقة لصنف معيّن، فإنها تصل إلى جميع أعضائه المحميّين (protected) والخواص (private)، انظر:
// تصريح مسبق للدوال void friend_function(); void non_friend_function(); class PrivateHolder { public: PrivateHolder(int val): private_value(val) {} private: int private_value; // إعلان إحدى الدوال صديقة friend void friend_function(); }; void non_friend_function() { PrivateHolder ph(10); // Compilation error: private_value is private. std::cout << ph.private_value << std::endl; } void friend_function() { // يُسمح للأصدقاء بالدخول إلى القيم الخاصة PrivateHolder ph(10); std::cout << ph.private_value << std::endl; }
مُعدِّلات الوصول لا تغيّر الدلالات الصديقة، وستكون الأعضاء العامة والمحمية والخاصّة لصنفٍ صديقٍ متكافئة كلها. كذلك لا تُورّث تصريحات الصداقة (Friend declarations)، فإن أنشأنا مثلًا صنفًا فرعيا من PrivateHolder
… :
class PrivateHolderDerived: public PrivateHolder { public: PrivateHolderDerived(int val): PrivateHolder(val) {} private: int derived_private_value = 0; };
… ثم حاولنا الوصول إلى أعضائه، سنحصل على ما يلي:
void friend_function() { PrivateHolderDerived pd(20); // OK std::cout << pd.private_value << std::endl; // Compilation error: derived_private_value is private. std::cout << pd.derived_private_value << std::endl; }
لاحظ أنّ دالة التابع PrivateHolderDerived
لا يمكنها الوصول إلى PrivateHolder::private_value
، في حين أنّ الدوال الصديقة تستطيع ذلك.
التوابع الصديقة
يمكن جعل التوابع صديقة تمامًا مثل الدوال، انظر:
class Accesser { public: void private_accesser(); }; class PrivateHolder { public: PrivateHolder(int val): private_value(val) {} friend void Accesser::private_accesser(); private: int private_value; }; void Accesser::private_accesser() { PrivateHolder ph(10); // الإعلان عن هذا التابع كصديق std::cout << ph.private_value << std::endl; }
الصنف الصديق (Friend class)
يمكن الإعلان عن صنف بأكمله كصديق، ويعني ذلك أنّه يجوز لأيّ عضو من أعضاء الصنف الصديق الوصول إلى الأعضاء الخاصّة والمحمية للصنف الآخر، انظر:
class Accesser { public: void private_accesser1(); void private_accesser2(); }; class PrivateHolder { public: PrivateHolder(int val): private_value(val) {} friend class Accesser; private: int private_value; }; void Accesser::private_accesser1() { PrivateHolder ph(10); // OK std::cout << ph.private_value << std::endl; } void Accesser::private_accesser2() { PrivateHolder ph(10); // OK std::cout << ph.private_value + 1 << std::endl; }
لا يمكن عكس التصريح بالصداقة بين الأصناف، فإن كان صنف A صديقًا لصنف B، فإنّ B لن يصبح تلقائيًّا صديقًا للصنف A. وإذا احتاجت الأصناف وصولًا خاصًا في كلا الاتجاهين فسيحتاجان كليهما إلى تصريحات صداقة. انظر:
class Accesser { public: void private_accesser1(); void private_accesser2(); private: int private_value = 0; }; class PrivateHolder { public: PrivateHolder(int val): private_value(val) {} friend class Accesser; void reverse_accesse() { Accesser a; std::cout << a.private_value; } private: int private_value; };
في المثال السابق، لاحظ كيف أن Accessor
صديق للصنف PrivateHolder
، لكن الأخير لا يستطيع الوصول إلى أعضاء الأول.
مصادقة أعضاء صنف
يوضّح المثال التالي كيفيّة مُصادَقة أعضاء صنف ما، حيث تكون A::Func1
دالة صديقة للصنف B
، لذا يمكنها الدخول إلى جميع أعضاء B
.
class B; class A { public: int Func1(B& b); private: int Func2(B& b); }; class B { private: int _b; friend int A::Func1(B& ); }; int A::Func1(B& b) { return b._b; } // OK int A::Func2(B& b) { return b._b; } // C2248
typename
-
عند إلحاق اسم مؤهَّل بكلمة
typename
فإنها تشير إلى أنها اسم لنوع، وغالبًا ما يكون هذا ضروريًا في القوالب، خاصة عندما يكون مُحدِّد الاسم المتشعّب (nested name specifier) نوعًا غير مستقل يخالف الاستنساخ الحالي. انظر المثال التالي حيث يعتمدstd::decay<T>
على معامل القالبT
، فنحتاج إلى إسباق الاسم المؤهَّل كله بكلمةtypename
المفتاحية من أجل تسمية النوع المتشعّبtype
. راجع هذا الرابط لتعرف أين تضع كلماتtemplate
وtypename
وأين أيضًا:
template <class T> auto decay_copy(T&& r) -> typename std::decay<T>::type;
-
تقدّم
typename
معامِلَ نوعٍ (type parameter) في تصريح القالب، وهي تماثلclass
في هذا السياق.
template < typename T > const T& min(const T& x, const T & y) { return b < a ? b : a; }
الإصدار ≥ C++ 17
-
يمكن أيضًا استخدام
typename
عند التصريح عن معامِل قالب القالب (template template parameter)، سابقة بهذا اسم المعامل تمامًا مثلclass
.
template < template < class T > typename U > void f() { U<int>::do_it(); U<double>::do_it(); }
explicit
عند تطبيق الكلمة المفتاحية explicit
على مُنشئ ذي وسيط واحد، فإنها تمنع أن يُستخدَم ذلك المُنشئ لإجراء أيّ تحويلات ضمنية.
class MyVector { public: explicit MyVector(uint64_t size); }; MyVector v1(100); // OK uint64_t len1 = 100; MyVector v2 { len1 }; // uint64_t من النوع len1 int len2 = 100; MyVector v3 { len2 }; // uint64_t إلى int غير مسموح لأنه تحويل ضمني من النوع
منذ إدخال قوائم المهيئات (initializer lists) في C++ 11، صار بالإمكان تطبيق explicit
على أيّ مُنشئ بغضّ النظر عن عدد وسائطه. انظر:
struct S { explicit S(int x, int y); }; S f() { return {12, 34}; // صيغة غير صحيحة. return S{12, 34}; // ok }
الإصدار ≥ C++ 11
عندما تُطبّق على دالّة تحويل (conversion function)، فإنّها تمنع استخدام تلك الدالّة لإجراء أيّ تحويلات ضمنية.
class C { const int x; public: C(int x) : x(x) {} explicit operator int() { return x; } }; C c(42); int x = c; // غير صحيح. int y = static_cast<int> (c); // صيغة صحيحة لأنه تحويل صريح.
sizeof
sizeof
هو عامل أحادي يعيد حجم معامَله بالبايت، وقد يكون يكون ذلك المعامَل تعبيرًا أو نوعًا، وإذا كان المعامَل تعبيرًا فلن يُقيَّم، وسيكون الحجم تعبيرًا ثابتًا من النوع std::size_t
. أما إن كان العامل نوعًا، فيجب أن يوضع بين قوسين.
-
لا يجوز تطبيق
sizeof
على نوع دالّة (function type). -
لا يجوز تطبيق
sizeof
على نوع غير مكتمل، بما في ذلكvoid
. -
إذا طُبِّق
sizeof
على نوع مرجعي مثلT&
أوT&&
، فسيكون مكافئًا لـsizeof(T)
. -
عندما تُطبّق
sizeof
على نوع صنف فإنها تعيد عدد البايتات في كائن كامل من ذلك النوع، بما في ذلك أيّ بايت للحشو (padding bytes) في المنتصف أو في النهاية. لذا لا تساويsizeof
القيمة صفر أبدًا. -
حجم كل من النوع
char
وsigned char
وunsigned char
يساوي واحد، لكن تذكّر أنّ البايت هو مقدار الذاكرة المطلوبة لتخزين كائن من النوعchar
، وهذا لا يعني بالضرورة أنه يساوي 8 بتّات (bits) لأنّ بعض الأنظمة تخزِّن كائناتchar
في مساحة أكبر من 8 بتات.
كذلك، إذا كان expr
تعبيرًا، فإنّ sizeof(
expr )
تكافئ sizeof(T)
، حيث يمثّل T
نوع التعبير expr
.
int a[100]; std::cout << "The number of bytes in `a` is: " << sizeof a; memset(a, 0, sizeof a);
الإصدار ≥ C++ 11
يعيد العامل sizeof...
عدد العناصر في حُزمة معامِلات ما.
template < class...T > void f(T && ...) { std::cout << "f was called with " << sizeof...(T) << " arguments\n"; }
noexcept
الإصدار ≥ C++ 11
-
noexcept
هو معامل أحاديٌّ يحدّد إمكانية نشرُ اعتراض كنتيجة لتقييم عامِله، لاحظ أنّ متون الدوال المُستدعاة لا تُفحَص، وعليه فقد تؤديnoexcept
إلى نتائج غير متوقّعة، بالمثل فإن العامل لا يُقيَّم.
#include <iostream> #include <stdexcept> void foo() { throw std::runtime_error("oops"); } void bar() {} struct S {}; int main() { std::cout << noexcept(foo()) << '\n'; // يطبع صفرًا std::cout << noexcept(bar()) << '\n'; // يطبع صفرًا std::cout << noexcept(1 + 1) << '\n'; // يطبع واحدًا std::cout << noexcept(S()) << '\n'; // يطبع واحدًا }
رغم أنّ bar()
في المثال السابق لن تطلق أيّ اعتراض إلا أنّ noexcept(bar())
تبقى خاطئة (false)، والسبب أنّه لم يُحدَّد بشكل صريح أنّ bar()
غير قادرة على نشر اعتراض.
-
تحدّد
noexcept
عند التصريح عن دالّة إن كانت تلك الدالّة قادرة على نشر اعتراض أم لا، وإذا استُخدِمت وحدَها فإنها تعلن أنّ الدالة لا يمكنها نشر أيّ اعتراض، وإذا استُخدِمت مع وسيط موضوع بين قوسين فإنها تحدّد إن كانت الدالّة تستطيع أن تنشر اعتراض اعتمادًا على قيمة الوسيط الحقيقية.
void f1() { throw std::runtime_error("oops"); } void f2() noexcept(false) { throw std::runtime_error("oops"); } void f3() {} void f4() noexcept {} void f5() noexcept(true) {} void f6() noexcept { try { f1(); } catch (const std::runtime_error&) {} }
في هذا المثال، صرّحنا بأنّ f4
و f5
و f6
لا يمكنها نشر الاعتراضات (Exceptions) -رغم أنّه يمكن إطلاق اعتراض أثناء تنفيذ f6
، إلا أنّه سيُحصَر ولن يُسمح له بالانتشار خارج الدالّة-، وصرحنا كذلك أن f2
تستطيع نشر اعتراض. ويكافئ حذف مُحدِّدات noexcept
التعبيرَ noexcept(false)
، لذلك فقد صرحنا ضمنيًا أنّ f1
و f3
يمكنهما نشر الاعتراضات رغم أنّه لا يمكن إطلاق الاعتراضات أثناء تنفيذ f3
.
الإصدار ≥ C++ 17
تتوقف قدرة دالة ما في حصر الاعتراضات (noexcept
) على نوع تلك الدالة، ففي المثال أعلاه، تختلف أنواع f1
و f2
و f3
عن f4
و f5
و f6
، لذلك فإنّ noexcept
مهمّة في مؤشّرات الدوالّ ووسائط القوالب وغير ذلك.
void g1() {} void g2() noexcept {} void( *p1)() noexcept = &g1; // غير صالح void( *p2)() noexcept = &g2; // تطابق الأنواع void( *p3)() = &g1; // تطابق الأنواع void( *p4)() = &g2; // تحويل ضمني
هذا الدرس جزء من سلسلة دروس عن C++.
ترجمة -بتصرّف- للفصل Chapter 23: Keywords من كتاب C++ Notes for Professionals.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.