سلسلة ++c للمحترفين الدرس 14: أهم الكلمات المفتاحية (Keywords) ودلالاتها في Cpp


محمد الميداوي

الكلمات المفتاحية هي كلمات لها معنى محدّد في 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 المفتاحية ما يلي:

  1. تقديم تعريف نوع الصنف:
class foo {
    int x;
    public:
        int get_x();f
    void set_x(int new_x);
};
  1. تقديم مُحدِّد مفصّل للنوع يشير إلى أنّ الاسم التالي هو اسم لنوع صنف (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();
};
  1. تقديم نوع المعاملات في تصريح القالب.
template < class T >
    const T& min(const T& x, const T& y) {
        return b < a ? b : a;
    }
  1. في التصريح عن معامل قالب القالب (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();
    }
  1. لاحظ أنّه يمكن الجمع بين النقطتين الثانية والثالثة في نفس التصريح، انظر المثال التالي حيث لا يُشترط أن تكون bar قد ظهرت من قبل:
template < class T >
    class foo {};
foo < class bar > x; 

الإصدار ≥ C++‎ 11.

  1. في تصريح أو تعريف نوع عددي enum، تصرّح أن التعداد هو تعدادٌ نطاقي (scoped enum):
enum class Format {
    TEXT,
    PDF,
    OTHER,
};
Format f = F::TEXT;

enum

هناك عدة أدوار للكلمة المفتاحية enum، وهي كالتالي:

  1. تقديم تعريف لنوع عددي.
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;
  1. تقديم مُحدّد مفصَّل للنوع يشير إلى أنّ الاسم التالي هو اسم نوع عددي سبق التصريح عنه. (لا يمكن استخدام مُحدّد مفصَّل للنوع في تصريح لاحق لنوع عددي)، ويمكن تسمية نوع عددي بهذه الطريقة حتى لو كان يخفيه اسم آخر. انظر في المثال التالي كيف أن صيغة Foo foo = FOO غير صحيح لأن Foo تشير إلى الدالة، بينما enum Foo foo = FOO صحيحة لأنها تشير إلى النوع العددي.
enum Foo {
    FOO
};
void Foo() {}
Foo foo = FOO; 
enum Foo foo = FOO;

الإصدار ≥ C++‎ 11

  1. تقديم تصريح عن نوع عددي مبهم يصرّح عن نوع عددي لكن دون تعريفه، وقد يعيد التصريح عن نوع عددي سبق التصريح عنه، ويمكن كذلك أن يُصرِّح لاحقًا (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، وهي كالتالي:

  1. تقديم تعريف نوع الاتحاد (union type) :
// POSIX مثال من
union sigval {
    int        sival_int;
    void        *sival_ptr;
};
  1. تقديم محدِّد مفصّل للنوع يشير إلى أنّ الاسم التالي هو اسم لنوع اتحاد، وإذا صُرِّح عن اسم الاتحاد سلفًا فيمكن العثور عليه حتى لو كان يخفيه اسم آخر، وإلا فسيكون مُصرَّحا عنه بشكل لاحق (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‎، ولا مصفوفات من هذا النوع ولا حتى مرجع إليه، وإنّما يُستخدم كنوع إعادة للدوال التي لا تُعيد أيّ شيء.

  1. عند استخدام void كنوع للقيمة المعادة من دالة فإنّها تشير إلى أنّ تلك الدالة لا تُعيد أيّ قيمة، وعند استخدامها في قائمة معاملاتِ دالةٍ فإنها تدلّ على أنّ تلك الدالة لا تأخذ أيّ معاملات (مثلًا: ‎int main()‎ و ‎int main(void)‎ متكافئتان) وأُجيزَت هذه الصياغة للتوافق مع C حيث تصريحات الدوال لها معنى مختلف عن مثيلاتها C++‎. أما عند استخدامها في تصريح مؤشّر فذلك يعني أنّ ذلك المؤشّر عام (universal).
  2. إذا كان المؤشّر من النوع void *‎ فإنّه يستطيع الإشارة إلى أيّ متغير لم يُصرّح عنه باستخدام const أو volatile، ولا يمكن تحصيل (dereferencing) مؤشّر فارغ (من النوع void *‎) إلا إن حُوِّل إلى نوع آخر. كذلك يمكن تحويل مؤشر فارغ إلى أيّ نوع آخر من مؤشّرات البيانات. وهذه الميزة تجعل النوع ‎void*‎ مناسبًا لأنواع معيّنة من واجهات مسح الأنواع (type-erasing interfaces) غير الآمنة (type-unsafe)، مثل السياقات العامة في الواجهات البرمجية (API) الشبيهة بـC مثل ‎qsort‎ و ‎pthread_create‎.
  3. يمكن أن يشير المؤشّر الفارغ إلى دالة، ولكن ليس إلى عضو من صنف في 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

  1. تُستخدم عبارات try و throw و catch لمُعالجة الاعتراضات (Exceptions) في C++‎،
  2. أولًا، استخدم كتلة try لتضع فيها تعليمة أو أكثر من التي قد تطلق اعتراضًا.
  3. يشير تعبير throw إلى أنّ شيئًا اعتراضيًا قد حدث في كتلة try -غالبًا ما يكون خطأ-، ويمكنك استخدام كائن من أيّ نوع كمعامَل لـ throw، ويُستخدم هذا الكائن لتوصيل معلومات بخصوص الخطأ. ويوصى في معظم الحالات باستخدام الصنف std::exception أو أحد الأصناف المشتقة منه والمُعرّفة في المكتبة القياسية، أما إذا لم تكن تلك الاستثناءات مناسبة فيمكنك اشتقاق اعتراض خاص بك من std::exception.
  4. استخدم كتلةَ 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) لها الوصول إلى تلك التفاصيل دون المساس بتغليف المعلومات.

  1. يكون من المفيد أحيانًا أن تمنح حقّ الوصول لأعضاءِ صنفٍ ما إلى دالةٍ ليست من أعضاء ذلك الصنف، أو إلى جميع الأعضاء في صنف آخر، ولا يمكن إلّا لمنفِّذ الصنف أن يصرح عن أصدقائه، ولا يمكن أن يصرح صنف أنه صديق لصنف آخر، وبالمثل لا يمكن لدالة أن تصرح عن ذلك. استخدام كلمة friend واسم دالة غير عضو أو اسم صنف آخر في تعريف الصنف، وذلك لمنحه حق الوصول إلى الأعضاء الخاصّين والمحميّين في ذلك الصنف، كما يمكن التصريح عن معامِل نوع كصديق في تعريف قالب ما.
  2. إذا صرحت عن دالة صديقة لم يسبق التصريح عنها، فستُصدَّر تلك الدالة إلى النطاق المحيط غير الصنفي (enclosing nonclass scope).
class friend F
friend F;
class ForwardDeclared; // اسم الصنف معروف
class HasFriends {
    friend int ForwardDeclared::IsAFriend(); // C2039 خطأ
};

الدوال الصديقة (friend functions)

  1. الدالة الصديقة هي دالة ليست عضوًا في صنف، ولكن لها حق الوصول إلى الأعضاء المحميّين والخواصّ في ذلك الصنف، ولا تُعدّ الدوال الصديقة من أعضاء الصنف بل هي دوال خارجية طبيعية تُمنح امتيازات وصول خاصّة.
  2. لا تدخل الدوال الصديقة في نطاق الصنف ولا تُستدعى باستخدام عوامل اختيار الأعضاء . و ‎->‎ إلّا إن كانت أعضاء من صنف آخر.
  3. يُصرَّح عن الدالّة الصديقة من قِبل الصنف الذي يمنح حق الوصول، ويمكن وضع تصريح الصداقة في أيّ مكان في تصريح الصنف، ولا يتأثّر التصريح بالكلمات المفتاحيّة الخاصّة المسؤولة عن التحكم في الوصول.
#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

  1. عند إلحاق اسم مؤهَّل بكلمة ‎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;
  1. تقدّم ‎typename‎ معامِلَ نوعٍ (type parameter) في تصريح القالب، وهي تماثل class في هذا السياق.
template < typename T >
    const T& min(const T& x,
        const T & y) {
        return b < a ? b : a;
    }

الإصدار ≥ C++‎ 17

  1. يمكن أيضًا استخدام ‎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

  1. 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()‎ غير قادرة على نشر اعتراض.

  1. تحدّد 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.





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن