سلسلة ++c للمحترفين الدرس 26: فضاءات الأسماء (Namespaces) في Cpp


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

تُستخدم فضاءات الأسماء (Namespaces) لمنع التضارب الذي قد يحدث عند استخدام عدّة مكتبات في نفس البرنامج، وفضاء الاسم سابقةٌ (prefix) تعريفية للدوالّ والأصناف والأنواع وغيرها.

ما هي فضاءات الاسم؟

فضاء الاسم في لغة ++C هو مجموعة من كيانات C++‎ سواءً كانت دوالًا أو أصنافًا أو متغيرات، وتُسبق أسماءها باسم "فضاء الاسم"، وعند كتابة شيفرة ضمن فضاء الاسم فلا يلزم إسباق الكيانات المسمّاة التي تنتمي إلى فضاء الاسم باسم ذلك الفضاء، أمّا الكيانات الموجودة خارجه فيجب أن تُؤهّل أسماؤها (أي تُسبق باسم "فضاء الاسم").

الاسم المؤهّل يأخذ الشكل التالي ‎<namespace>::<entity>‎. انظر الشيفرة التالية:

namespace Example {
    const int test = 5;
    const int test2 = test + 12;        // `Example` تعمل داخل فضاء الاسم
}
const int test3 = test + 3;             // غير موجودة خارج فضاء الاسم `test` خطأ، لأن
const int test3 = Example::test + 3;    // هذا يعمل لأنّ المتغير مؤهل

فضاءات الاسم مفيدة لتجميع التعريفات المرتبطة ببعضها معًا، ويمكن تشبيهها بمراكز التسوف التي تُقسّم إلى عدة متاجر يبيع كل منها سلعًا من صنف محدد، فقد يكون أحد المتاجر متخصصًأ في الإلكترونيات بينما يكون مجال الآخر في بيع الأحذية.

هذا التصنيف المنطقي لأنواع المتاجر يساعد المتسوّقين على العثور على السلع التي يبحثون عنها. وبالمثل فإن فضاءات الاسم تساعد مبرمجي C++‎ في العثور على الدوالّ والأصناف والمتغيرات التي يبحثون عنها من خلال تنظيمها بطريقة منطقية. مثلًا:

namespace Electronics {
    int TotalStock;
    class Headphones {
        // (color, brand, model number) وصف السماعات
    };
    class Television {
        // (color, brand, model number) وصف التلفاز
    };
}
namespace Shoes {
    int TotalStock;
    class Sandal {
        // (color, brand, model number) وصف الصندل
    };
    class Slipper {
        // (color, brand, model number) وصف الخُف
    };
}

لدينا فضاء اسم معرّف مسبقًا وهو فضاء الاسم العام ( global namespace)، وليس لهذا الفضاء اسم، لكن يُرمَز إليه بـ ‎::‎. مثلًا:

void bar() {
    // مُعرّفة في فضاء الاسم العام
}
namespace foo {
    void bar() {
        // foo مُعرّفة في فضاء الاسم  
    }
    void barbar() {
        bar(); // foo::bar() استدعاء
        ::bar(); // المُعرّفة في فضاء الاسم العام  bar() استدعاء
    }
}

البحث القائم على الوسائط

عند استدعاء دالّة ليس لها مؤهّلُ فضاءِ اسمٍ (namespace qualifier) صريح، فإن للمصرّف الحرية في استدعاء تلك الدالّة داخل فضاء اسم ما إن وُجد أحد أنواع المعاملات الخاصّة بتلك الدالّة في هذا الفضاء، وهذا يُسمّى "البحث القائم على الوسائط" (Argument Dependent Lookup أو ADL)، انظر:

namespace Test
{
    int call(int i);
    class SomeClass {...};

    int call_too(const SomeClass & data);
}

call(5); // خطأ: اسم دالّة غير مؤهل

Test::SomeClass data;

call_too(data); // ناجح.

تفشل ‎call‎ لأنّ أنواع المعاملات الخاصة بها لا تنتمي إلى فضاء الاسم ‎Test‎، على عكس ‎call_too‎ التي تعمل بنجاح لأنّ ‎SomeClass‎ عضو في ‎Test‎، ومن ثم فإنه مؤهّل للبحث القائم على الوسائط.

ما الذي يمنع البحث القائم على الوسائط (ADL)

لا يحدث البحث القائم على الوسائط إذا كان البحث العادي غير المؤهّل (normal unqualified lookup) يجد عضوًا من صنف أو دالّة مُصرَّحة في نطاق الكتلة أو كائنًا من غير نوع الدالّة. انظر:

void foo();
namespace N {
struct X {};
void foo(X ) { std::cout << '1'; }
void qux(X ) { std::cout << '2'; }
}

struct C {
void foo() {}
void bar() {
           // لا تأخذ أي وسائط C::foo() خطأ: البحث القائم على الوسائط معطّل والدالّة
foo(N::X{}); 
}
};
void bar() {
extern void foo(); // ‫إعادة تعريف ‎::foo
 // لا تأخذ أي وسائط ::foo() خطأ: البحث القائم على الوسائط معطّل والدالّة
foo(N::X{});
}

int qux;

void baz() {
qux(N::X{}); // qux خطأ: إعلان المتغير يعطل البحث القائم على الوسائط بالنسبة لـ 
}

توسيع فضاءات الاسم

إحدى الميزات المفيدة لفضاءات الاسم ‎namespace‎ هو أنّه يمكنك توسيعها، أي إضافة أعضاء إليها.

namespace Foo {
    void bar() {}
}
// أشياء أخرى
namespace Foo {
    void bar2() {}
}

المُوجّه 'using'

كلمة using المفتاحية لها ثلاث نكهات، وعندما نجمعها مع كلمة namespace فإننا نكتب موجِّهَ using أو (Using Directive)، فإذا لم ترد أن تكتب ‎Foo::‎ قبل كلّ كيان في فضاء الاسم ‎Foo‎، فيمكنك استخدام التعبير ‎using namespace Foo;‎ لاستيراد كل ما تحتويه ‎Foo‎. انظر:

namespace Foo {
    void bar() {}
    void baz() {}
}
// Foo::bar() ينبغي استخدام
Foo::bar();
// Foo استيراد
using namespace Foo;
bar(); // جيد
baz(); // جيد

من الممكن أيضًا استيراد كيانات محدّدة في فضاء الاسم بدلاً من استيراد فضاء الاسم بأكمله:

using Foo::bar;
bar(); // جيد، تم استيراده بنجاح.
baz(); // خطأ، لم يُستورَد.

تحذير: غالبًا ما يُعدّ وضع ‎using namespace‎ في ملفات الترويسة من الممارسات السيئة لأنّ ذلك سيؤدي إلى استيراد فضاء الاسم في كلّ ملف يتضمن تلك الترويسة. ونظرًا لعدم وجود أيّ طريقة لتعطيل استيراد فضاء الاسم، فقد يؤدي ذلك إلى تلويث فضاء الاسم (أي حشو فضاء الاسم العام برموز كثيرة أو غير متوقعة)، أو قد يؤدّي إلى التضارب بين الأسماء. انظر المثال التالي لتوضيح هذا الأمر:

/***** foo.h *****/
namespace Foo {
    class C;
}
/***** bar.h *****/
namespace Bar {
    class C;
}
/***** baz.h *****/
#include "foo.h"

using namespace Foo;
/***** main.cpp *****/
#include "bar.h"

#include "baz.h"

using namespace Bar;
C c; // Foo::C و  Bar::C خطأ: تضارب بين

كما ترى، فلا يمكن استخدام الموجّه using في نطاق الصنف.

التصريح عبر using

عندما تصرّح عن كائنٍ ما باستخدام ‎using‎ فسيؤدّي ذلك إلى إدخال اسم سبق تصريحه في موضع آخر إلى النطاق الحالي.

استيراد الأسماء فرديًّا من فضاء اسم

انظر المثال التالي:

#include <iostream>
int main() {
    using std::cout;
    cout << "Hello, worlبd!\n";
}

عند استخدام ‎using‎ لإدخال الاسم ‎cout‎ من فضاء اسم ‎std‎ في نطاق دالّة ‎main‎، سنتمكن من الإشارة إلى الكائن std::cout بالاسم cout فقط.

إعادة التصريح عن أعضاء صنف أساسي لتجنّب إخفاء الأسماء

لا يُسمح بإعادة تعريف الأعضاء خلا أعضاء الصنف الأساسي عند التصريح عن أعضاء عبر using في نطاق الصنف. على سبيل المثال، ‎using std::cout‎ غير مسموح بها في نطاق الصنف.

الاسم المُعاد تصريحه كان سيُخفى غالبًا، على سبيل المثال، تشير ‎d1.foo‎ في المثال أدناه إلى ‎Derived1::foo(const char*)‎ وحسب، لذلك سيحدث خطأ في التصريف.

مسألة أنّ الدالّة ‎Base::foo(int)‎ ستُخفى ليس لها تأثير. لكن من الناحية الأخرى فإن ‎d2.foo(42)‎ صحيحة لأنّ التصريح باستخدام using يُدرِج ‎Base::foo(int)‎ في مجموعة الكيانات المُسمّاة ‎foo‎ في ‎Derived2‎. ستنتهي عملية البحث عن الأسماء بإيجاد كلا الدالّتين المُسمّاتين ‎foo‎، وبعد تحليل زيادة التحميل (overload resolution)، يُختار ‎Base::foo‎.

struct Base {
    void foo(int);
};
struct Derived1: Base {
    void foo(const char * );
};
struct Derived2: Base {
    using Base::foo;
    void foo(const char * );
};
int main() {
    Derived1 d1;
    d1.foo(42); // خطأ
    Derived2 d2;
    d2.foo(42); // صحيح
}

وراثة المُنشئات

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

كحالة خاصة، يمكن أن يشير استخدام using للتصريح في نطاق الصنف إلى مُنشئات (constructors) صنف أبٍ مباشر (direct base class)، ثم تُورَّث تلك المُنشئات إلى الصنف المشتقّ، ويمكن استخدامها لتهيئة ذلك الصنف. انظر:

struct Base {
    Base(int x,
        const char * s);
};
struct Derived1: Base {
    Derived1(int x,
        const char * s): Base(x, s) {}
};
struct Derived2: Base {
    using Base::Base;
};
int main() {
    Derived1 d1(42, "Hello, world");
    Derived2 d2(42, "Hello, world");
}

في الشيفرة أعلاه، يحتوي كل من ‎Derived1‎ و ‎Derived2‎ على مُنشئات تعيد توجيه الوسائط مباشرةً إلى المُنشئ المقابل للصنف ‎Base‎. وينفّذ الصنف ‎Derived1‎ إعادة التوجيه بشكل صريح، بينما يستخدم الصنف ‎Derived2‎ ميزة توريث المُنشئات الموجودة في C++‎ 11 لفعل ذلك ضمنيًا.

إنشاء فضاءات الاسم

من السهل إنشاء فضاءات الاسم، انظر المثال التالي إذ سننشئ فضاء الاسم foo ثم نصرح عن الدالة bar فيه:

namespace Foo {
    void bar() {}
}

لاستدعاء ‎bar‎، يجب عليك تحديد فضاء الاسم أولاً، متبوعًا بعامل تحليل النطاق (scope resolution operator‏‏) وهو ‎::‎:

Foo::bar();

يُسمح بإنشاء فضاء اسمٍ داخل فضاء اسم آخر، مثلًا:

namespace A {
    namespace B {
        namespace C {
            void bar() {}
        }
    }
}

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

يمكن تبسيط الشيفرة أعلاه على النحو التالي:

namespace A::B::C {
    void bar() {}
}

فضاءات الاسم غير المُسمّاة أو المجهولة

يمكن استخدام فضاء اسم غير مُسمَّى (unnamed namespace) لضمان وجود ارتباط داخلي بين الأسماء التي لا يمكن الإشارة إليها إلّا بوحدة الترجمة الحالية (translation unit)، وتُعرّف فضاءات الاسم غير المسمّاة بنفس طريقة تعريف فضاءات الاسم الأخرى، ولكن من دون اسم:

namespace {
    int foo = 42;
}

لن تكون ‎foo‎ مرئيّة إلّا في وحدة الترجمة التي تظهر فيها، ويوصى بعدم استخدام فضاءات-الاسم-غير-المُسمّاة في ملفات الترويسة (Header Files) لأنّ هذا سيعطي إصدارًا من المحتوى لكل وحدة ترجمة أُدرِج فيها، وهذا مهم جدًا خاصّة عند تعريف متغيرات-عامة-غير-ثابتة. انظر:

// foo.h
namespace {
    std::string globalString;
}

// 1.cpp
#include "foo.h" //< تولّد: unnamed_namespace{1.cpp}::globalString ...

globalString = "Initialize";

// 2.cpp
#include "foo.h" //< تولّد: unnamed_namespace{2.cpp}::globalString ...

std::cout << globalString; //< ستطبع دائما سلسلة نصية فارغة

فضاءات الاسم المضغوطة والمتشعبة

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

انظر المثال التالي:

namespace a {
    namespace b {
        template < class T >
            struct qualifies: std::false_type {};
    }
}

namespace other {
    struct bob {};
}

namespace a::b {
    template < >
        struct qualifies < ::other::bob >: std::true_type {};
}

ابتداءً من C++‎ 17، صار بإمكانك الدخول إلى فضاء الاسم ‎a‎ و كذلك ‎b‎ بخطوة واحدة عبر التعبير ‎namespace a::b‎.

كنية فضاء الاسم

يمكن إعطاء فضاء الاسم كُنيةً أو اسمًا بديلًا (alias)، أي اسمًا آخر يمثّل فضاء الاسم نفسه باستخدام الصيغة ‎namespace identifier =‎، ويمكن الوصول إلى أعضاء فضاء الاسم المُكَنَّى عبر تأهيلهم (أي إسباقهم) بالكُنية.

في المثال التالي، فضاء الاسم الُمتشعّب ‎AReallyLongName::AnotherReallyLongName‎ طويل جدًّا، لذا تصرح الدالّة ‎qux‎ عن الكُنية ‎N‎. يمكن الآن الوصول إلى أعضاء فضاء الاسم باستخدام ‎N::‎. انظر:

namespace AReallyLongName {
    namespace AnotherReallyLongName {
        int foo();
        int bar();
        void baz(int x, int y);
    }
}
void qux() {
    namespace N = AReallyLongName::AnotherReallyLongName;
    N::baz(N::foo(), N::bar());
}

فضاء الاسم المضمن (Inline namespace)

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

تُدرِج ‎inline ‎namespace‎ محتوى فضاء الاسم المُضمّن في فضاء الاسم المحيط (Enclosing)، لذلك فإنّ الشيفرة التالية:

namespace Outer {
    inline namespace Inner {
        void foo();
    }
}

تكافئ بشكل كبير الشيفرة أدناه:

namespace Outer {
    namespace Inner {
        void foo();
    }
    using Inner::foo;
}

غير أن عناصر ‎Outer::Inner::‎ وعناصر ‎Outer::‎ متطابقة، لذلك فإن السَّطرن التالين متكافئان:

Outer::foo();
Outer::Inner::foo();

لكنّ التعبير ‎using namespace Inner;‎ لن يكون مكافئًا لبعض الأجزاء مثل تخصيص القوالب (template specialization):

#include <outer.h> // انظر أسفله

class MyCustomType;
namespace Outer {
    template < >
        void foo < MyCustomType > () {
            std::cout << "Specialization";
        }
}

يسمح فضاء الاسم المُضمَّن بتخصيص ‎Outer::foo‎، انظر المثال التالي حيث أهملنا include guard من أجل التبسيط:

// outer.h
namespace Outer {
    inline namespace Inner {
        template < typename T >
            void foo() {
                std::cout << "Generic";
            }
    }
}

بينما لا يسمح التعبير ‎using namespace‎ بتخصيص ‎Outer::foo‎، انظر أدناه أيضًا حيث أهملنا include guard من أجل التبسيط:

// outer.h
namespace Outer {
    namespace Inner {
        template < typename T >
            void foo() {
                std::cout << "Generic";
            }
    }
    using namespace Inner;
    // `Outer::foo` لا يمكن تخصيص
    // `Outer::Inner::foo` الصيغة الصحيحة هي
}

تتيح فضاءات الاسم المُضمّنة لعدة إصدارات أن تتواجد في نفس الوقت بحيث تعود كلها افتراضيًّا إلى فضاء الاسم المُضمّن (‎inline‎):

namespace MyNamespace {
    // تضمين الإصدار الأخير
    inline namespace Version2 {
        void foo(); // إصدار جديد
        void bar();
    }
    namespace Version1 // الإصدار القديم
    {
        void foo();
    }
}

ومع استخدام:

MyNamespace::Version1::foo(); // الإصدار القديم
MyNamespace::Version2::foo(); // الاصدار الجديد
MyNamespace::foo(); // MyNamespace::Version1::foo(); الاصدار الافتراضي

تكنية فضاء اسم طويل

من الممكن تكنِية فضاءات الاسم الطويلة في ++C، وهو مفيد مثلًا في حال أردت الإشارة إلى مكوّنات مكتبة ما. انظر:

namespace boost {
    namespace multiprecision {
        class Number...
    }
}
namespace Name1 = boost::multiprecision;

// كلا التصريحين متكافئين
boost::multiprecision::Number X    // كتابة فضاء الاسم بأكمله
Name1::Number Y            // استخدام الكنية

نطاق تصريح الكنية

يتأثّر تصريح الكنية باستخدام تعليمات using الموجودة قبله:

namespace boost {
    namespace multiprecision {
        class Number...
    }
}

using namespace boost;

// فضاءا الاسم متكافئان
namespace Name1 = boost::multiprecision;
namespace Name2 = multiprecision;

لكن قد تكون عمليّة تكنية فضاء الاسم مربكة أحيانًا حول اختيار الفضاء الذي تريد وضع اسم بديل أو كنية له، كما في المثال التالي:

namespace boost {
    namespace multiprecision {
        class Number...
    }
}

namespace numeric {
    namespace multiprecision {
        class Number...
    }
}

using namespace numeric;
using namespace boost;

لا يُنصح بهذا لأنه ليس واضحًا إن كان Name1 يشير إلى numeric::multiprecision أم boost::multiprecision، نتابع:

namespace Name1 = multiprecision;
// يُفضل استخدام المسار الكامل
namespace Name2 = numeric::multiprecision;
namespace Name3 = boost::multiprecision;

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 44: Namespaces والفصل Chapter 46: Using declaration من كتاب C++ Notes for Professionals





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


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



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

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

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


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

تسجيل الدخول

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


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