اذهب إلى المحتوى

مواضيع متفرقة في Cpp تصقل من مهارة المبرمج وخبرته


محمد بغات

النطاقات 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


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...