سلسلة ++c للمحترفين الدرس 45: مواضيع متقدمة عن الأنواع والتعامل معها في Cpp


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

آلية RTTI: معلومات الأنواع في وقت التشغيل (Run-Time Type Information)

dynamic_cast

استخدم ‎dynamic_cast<>()‎ كدالة تساعدك على التخفيض النوعي (downcasting) في التسلسل الهرمي للوراثة (الوصف الرئيسي). وإذا كنت بحاجة إلى إجراء بعض الأعمال غير متعددة الأشكال (non-polymorphic) على صنفين مشتقّين ‎B‎ و ‎C‎ عبر الصنف الأب ‎class A‎، فستحتاج إلى كتابة ما يلي:

class A { public: virtual ~A(){} };

class B: public A
{ public: void work4B(){} };

class C: public A
{ public: void work4C(){} };

void non_polymorphic_work(A* ap)
{
    if (B* bp =dynamic_cast<B*>(ap))
        bp->work4B();
    if (C* cp =dynamic_cast<C*>(ap))
        cp->work4C();
}

الكلمة المفتاحية typeid

الكلمة المفتاحية ‎typeid‎ هي عامل أحادي يعطي معلومات حول النوع المُمرّر إليها في وقت التشغيل في حال كان معامَلها (operand) كائنًا من صنف متعدد الأشكال. تعيد ‎typeid‎ قيمةً يسارية من النوع ‎const std::type_info‎، كما تتجاهَل التأهيل عالي المستوى (Top-level cv-qualification).

struct Base
{
    virtual~Base() = default;
};
struct Derived: Base {};
Base *b = new Derived;
assert(typeid(*b) == typeid(Derived {}));    // حسنا

يمكن أيضًا تطبيق ‎typeid‎ على النوع مباشرة، ويتم تجريد مراجع المستوى الأعلى الأولى (first top-level references) في هذه الحالة، ثم يُتجاهَل التأهيل عالي المستوى. ومن ثم يمكن كتابة المثال أعلاه باستخدام ‎typeid(Derived)‎ بدلاً من typeid(Derived{})‎:

assert(typeid(*b) == typeid(Derived {}));    // OK

إذا طُبِّقت ‎typeid‎ على تعبير من غير النوع متعدد الأشكال فلن يُقيَّم المعامَل، أمّا معلومات النوع المُعادة فستخصّ النوع الساكن.

struct Base
{
    // ملاحظة: لا مدمّرات وهمية
};
struct Derived: Base {};
Derived d;
Base &b = d;
assert(typeid(b) == typeid(Base));    // غير مشتق
assert(typeid(std::declval<Base> ()) == typeid(Base));    // لابأس، لأنّه غير مُقيَّم

أسماء الأنواع

تستطيع الحصول على الاسم المعرَّف من قِبل التنفيذ لنوع معيّن في وقت التشغيل باستخدام الدالة التابعة ‎.name()‎ الخاص بالكائن ‎std::type_info‎ المُعاد من قِبل ‎typeid‎.

#include <iostream>
#include <typeinfo>

int main()
{
    int speed = 110;

    std::cout << typeid(speed).name() << '\n';
}

يكون الخرج ما يلي (متعلق بالتنفيذ):

int

كيف تعرّف التحويل الذي ينبغي استخدامه

  • استخدم التحويل الديناميكي dynamic_cast لتحويل المؤشّرات/المراجع داخل التسلسل الهرمي للوراثة.

  • استخدم التحويل الساكن static_cast لإجراء تحويلات الأنواع العادية.

  • استخدم تحويل إعادة التفسير reinterpret_cast لإعادة تفسير أنماط البتات منخفضة المستوى، لكن استخدمه بحذر شديد.

  • استخدم التحويل الثابت const_cast للتخلص من الثباتيّة أو التغايرية (const/volatile). تجنّب هذا الخيار ولا تستخدمه إلّا كنت مضطرًّا لاستخدام واجهة برمجية غير صحيحة ثباتيًّا (const-incorrect API).

شطب الأنواع Type Erasure

شطب النوع (Type Erasure) هو مجموعة من التقنيات الهادفة لإنشاء نوع يمكن أن يوفّر واجهة موحّدة للأنواع الأساسية (underlying types)، مع إخفاء معلومات النوع الأساسي عن العميل. وتُعدُّ ‎std::function<R(A...)>‎، التي يمكنها تخزين كائنات قابلة للاستدعاء من مختلف الأنواع، أفضل مثال معروف على شطب الأنواع في C++‎.

std::function للنقل فقط

شطب النوع std::function ينحصر في عدد قليل من العمليات، وأحد الأشياء التي يتطّلّبها الشطب أن تكون القيمة المخزّنة قابلة للنسخ. لكن هذا قد يتسبّب بمشاكل في بعض السياقات، كما في حالة تخزين تعابير لامبدا للمؤشرات الحصريّة (unique ptrs)، وقد يضيف هذا المتطلَّب حِملًا إضافيًا على البرنامج إذا كنت تستخدم ‎std::function‎ في سياق لا يهمّ فيه النسخ، كساحة خيوط (thread pool) مثلًا، حيث توفد (dispatch) المهامّ إلى الخيوط.

الكائن ‎std::packaged_task<Sig>‎ هو كائن قابل للاستدعاء، كما أنه قابل للنقل فقط (move-only)، وتستطيع تخزين std::packaged_task<R(Args...)>‎ في std::packaged_task<void(Args...)>‎، إلا أنّها طريقة بطيئة لإنشاء صنف شطب للنوع (type-erasure class) يكون للنقل فقط (move-only) وقابلا للاستدعاء (callable) في نفس الوقت.

يوضّح المثال التالي كيف يمكنك كتابة نوع ‎std::function‎ بسيط، سنحذف مُنشئ النسخ - copy constructor - (والذي يتضمّن إضافة تابع ‎clone‎ إلى ‎details::task_pimpl<...>‎). سنضعه في فضاء اسم (namespace) إذ سيسمح لنا ذلك بتخصيصه للقيمة المعادة الفارغة void:

template < class Sig>
    struct task;

namespace details
{
    template < class R, class...Args >
        struct task_pimpl
        {
            virtual R invoke(Args && ...args) const = 0;
            virtual~task_pimpl() {};
            virtual
            const std::type_info &target_type() const = 0;
        };
    // store an F.    invoke(Args&&...) calls the f
    template < class F, class R, class...Args >
        struct task_pimpl_impl: task_pimpl<R, Args... >
        {
            F f;
            template < class Fin>
            task_pimpl_impl(Fin && fin): f(std::forward<Fin> (fin)) {}

            virtual R invoke(Args && ...args) const final override
            {
                return f(std::forward<Args> (args)...);
            }

            virtual
            const std::type_info &target_type() const final override
            {
                return typeid(F);
            }
        };

سيتجاهل إصدار void القيمة التي تعيدها f، نتابع …

    template < class F, class...Args >
        struct task_pimpl_impl<F, void, Args... >: task_pimpl<void, Args... >
        {
            F f;
            template < class Fin>
            task_pimpl_impl(Fin && fin): f(std::forward<Fin> (fin)) {}

            virtual void invoke(Args && ...args) const final override
            {
                f(std::forward<Args> (args)...);
            }

            virtual
            const std::type_info &target_type() const final override
            {
                return typeid(F);
            }
        };
};
template < class R, class...Args >
    struct task < R(Args...) >
    {
        // semi-regular:
        task() = default;
        task(task &&) = default;
        // no copy
        private:

هنا ننشئ أسماءً بديلة أو كُنى (aliases) لتحسين مظهر شيفرة sfinae أدناه، …

            template < class F>
            using call_r = std::result_of_t < F
        const &(Args...) > ;
        template < class F>
        using is_task = std::is_same<std::decay_t < F>, task> ;
        public:
            // قابل للاستدعاء F يمكن تدميرها من كائن 
            template < class F,

        class= decltype( (R)(std::declval<call_r<F>>()) ),
        // ليس النوع نفسه
        std::enable_if_t<!is_task<F>{}, int>* = nullptr>
        task(F&& f):m_pImpl( make_pimpl(std::forward<F>(f)) ) {}


        R operator()(Args...args) const
        {
            return m_pImpl->invoke(std::forward<Args> (args)...);
        }

        explicit operator bool() const
        {
            return (bool) m_pImpl;
        }

        void swap(task & o)
        {
            std::swap(m_pImpl, o.m_pImpl);
        }

        template < class F>
        void assign(F && f)
        {
            m_pImpl = make_pimpl(std::forward<F> (f));
        }

        // std::function جزء من واجهة
        const std::type_info &target_type() const
        {
            if (! *this) return typeid(void);
            return m_pImpl->target_type();
        }

        template < class T>
        T* target()
        {
            return target_impl<T> ();
        }

        template < class T>
        const T* target() const
        {
            return target_impl<T> ();
        }

        // nullptr مقارنة مع
        friend bool operator==(std::nullptr_t, task
            const &self)
        {
            return !self;
        }

        friend bool operator==(task
            const &self, std::nullptr_t)
        {
            return !self;
        }

        friend bool operator!=(std::nullptr_t, task
            const &self)
        {
            return !!self;
        }

        friend bool operator!=(task
            const &self, std::nullptr_t)
        {
            return !!self;
        }

        private: template < class T>
            using pimpl_t = details::task_pimpl_impl<T, R, Args... > ;
        template < class F>
        static auto make_pimpl(F && f)
        {
            using dF = std::decay_t<F> ;
            using pImpl_t = pimpl_t<dF> ;
            return std::make_unique<pImpl_t> (std::forward<F> (f));
        }

        std::unique_ptr<details::task_pimpl<R, Args... >> m_pImpl;
        template < class T>
        T* target_impl() const
        {
            return dynamic_cast<pimpl_t<T> *> (m_pImpl.get());
        }
    };

ربما تود إضافة تحسين للمخزن المؤقت الصغير (Small Buffer Optimization) لهذه المكتبة كي لا تخزِّن كل الاستدعاءات في الكومة (heap). وستجد أنك محتاج إلى استخدام ‎task(task&&)‎ غير الافتراضية من أجل إضافة ذلك التحسين، كذلك ستحتاج إلى ‎std::aligned_storage_t‎ داخل الصنف، ومؤشّر حصري ‎unique_ptr‎ يشير إلى ‎m_pImpl‎ مع حاذف (deleter) يمكن ضبطه على خاصّية التدمير فقط (مع عدم إعادة الذاكرة إلى الكومة). أيضًا، ستحتاج إلى كتابة emplace_move_to( void* ) = 0‎ في ‎task_pimpl‎.

انظر هذا المثال الحي (بدون خوارزمية تحسين المخزن المؤقت الصغير SBO).

الشطب إلى نوع نمطي مع جدول (vtable) وهمي

تعتمد C++‎ على ما يُعرف بالأنواع النمطية - Regular types - (أو على الأقل شبه النمطية - Pseudo-Regular). والنوع النمطي هو نوع يمكن إنشاؤه والإسناد إليه أو منه عبر النسخ أو النقل، ويمكن تدميره، ويمكن مقارنته عبر معامل المساواة. ويمكن أيضًا أن يُنشأ بدون وسائط، كما يدعم بعض العمليات الأخرى المفيدة في خوارزميات وحاويات المكتبة القياسية ‎std‎.

اطلع إن شئت على هذا الرابط الأجنبي إلى الورقة الأصلية التي تأسس عليها هذا المفهوم. قد ترغب في إضافة دعم لـ ‎std::hash‎ في C++‎ 11.

وهنا، سنستخدم منهج الجدول الوهمي vtable لأجل شطب النوع (type erasure).

using dtor_unique_ptr = std::unique_ptr<void, void(*)(void*) > ;
template < class T, class...Args >
    dtor_unique_ptr make_dtor_unique_ptr(Args && ...args)
    {
        return {
            new T(std::forward<Args> (args)...), [](void *self)
            {
                delete static_cast<T*> (self);
            }
        };
    }

struct regular_vtable
{
    void(*copy_assign)(void *dest, void
        const *src);    // T&=(T const&)
    void(*move_assign)(void *dest, void *src);    // T&=(T&&)
    bool(*equals)(void
        const *lhs, void
        const *rhs);    // T const&==T const&
    bool(*order)(void
        const *lhs, void
        const *rhs);    // std::less<T>{}(T const&, T const&)
    std::size_t(*hash)(void
        const *self);    // std::hash<T>{}(T const&)
    std::type_info const &(*type)();    // typeid(T)
    dtor_unique_ptr(*clone)(void
        const *self);    // T(T const&)
};
template < class T>
    regular_vtable make_regular_vtable() noexcept
    {
        return {
        [](void *dest, void
                const *src)
            {*static_cast<T*> (dest) = *static_cast< T
                const*> (src);
            },
        [](void *dest, void *src)
            {*static_cast<T*> (dest) = std::move(*static_cast<T*> (src));
            },
        [](void
                const *lhs, void
                const *rhs)
            {
                return * static_cast< T
                const*> (lhs) == *static_cast< T
                const*> (rhs);
            },
        [](void
                const *lhs, void
                const *rhs)
            {
                return std::less < T> {}(*static_cast< T
                    const*> (lhs), *static_cast< T
                    const*> (rhs));
            },
        [](void
                const *self)
            {
                return std::hash < T> {}(*static_cast< T
                    const*> (self));
            },
        []()->decltype(auto)
            {
                return typeid(T);
            },
        [](void
                const *self)
            {
                return make_dtor_unique_ptr<T> (*static_cast< T
                    const*> (self));
            }
        };
    }

template < class T>
    regular_vtable
const* get_regular_vtable() noexcept
{
    static
    const regular_vtable vtable = make_regular_vtable<T> ();
    return &vtable;
}

struct regular_type
{
    using self = regular_type;
    regular_vtable
    const *vtable = 0;
    dtor_unique_ptr ptr
    {
        nullptr, [](void*) {}
    };

    bool empty() const
    {
        return !vtable;
    }

    template < class T, class...Args >
        void emplace(Args && ...args)
        {
            ptr = make_dtor_unique_ptr<T> (std::forward<Args> (args)...);
            if (ptr)
                vtable = get_regular_vtable<T> ();
            else
                vtable = nullptr;
        }

    friend bool operator==(regular_type
        const &lhs, regular_type
        const &rhs)
    {
        if (lhs.vtable != rhs.vtable) return false;
        return lhs.vtable->equals(lhs.ptr.get(), rhs.ptr.get());
    }

    bool before(regular_type
        const &rhs) const
    {
        auto
        const &lhs = *this;
        if (!lhs.vtable || !rhs.vtable)
            return std::less < regular_vtable
        const*> {}(lhs.vtable, rhs.vtable);
        if (lhs.vtable != rhs.vtable)
            return lhs.vtable->type().before(rhs.vtable->type());
        return lhs.vtable->order(lhs.ptr.get(), rhs.ptr.get());
    }

من الناحية الفنية، فإن >friend bool operator التي تستدعي before مطلوبة هنا، نتابع المثال …

    std::type_info
    const* type() const
    {
        if (!vtable) return nullptr;
        return &vtable->type();
    }

    regular_type(regular_type && o):
        vtable(o.vtable),
        ptr(std::move(o.ptr))
        {
            o.vtable = nullptr;
        }

    friend void swap(regular_type &lhs, regular_type &rhs)
    {
        std::swap(lhs.ptr, rhs.ptr);
        std::swap(lhs.vtable, rhs.vtable);
    }

    regular_type &operator=(regular_type && o)
    {
        if (o.vtable == vtable)
        {
            vtable->move_assign(ptr.get(), o.ptr.get());
            return * this;
        }

        auto tmp = std::move(o);
        swap(*this, tmp);
        return * this;
    }

    regular_type(regular_type
            const &o):
        vtable(o.vtable),
        ptr(o.vtable ? o.vtable->clone(o.ptr.get()) : dtor_unique_ptr
        {
            nullptr, [](void*) {} })
        {
            if (!ptr && vtable) vtable = nullptr;
        }

    regular_type &operator=(regular_type
        const &o)
    {
        if (o.vtable == vtable)
        {
            vtable->copy_assign(ptr.get(), o.ptr.get());
            return * this;
        }

        auto tmp = o;
        swap(*this, tmp);
        return * this;
    }

    std::size_t hash() const
    {
        if (!vtable) return 0;
        return vtable->hash(ptr.get());
    }

    template < class T,
       std::enable_if_t<!std::is_same<std::decay_t < T>, regular_type> {}, int>* = nullptr >
        regular_type(T && t)
        {
            emplace<std::decay_t < T>> (std::forward<T> (t));
        }
};
namespace std
{
    template < >
        struct hash < regular_type>
        {
            std::size_t operator()(regular_type
                const &r) const
            {
                return r.hash();
            }
        };
    template < >
        struct less < regular_type>
        {
            bool operator()(regular_type
                const &lhs, regular_type
                const &rhs) const
            {
                return lhs.before(rhs);
            }
        };
}

هذا مثال حيّ على ذلك.

يمكن استخدام مثل هذا النوع النمطي كمفتاح لقاموس ‎std::map‎ أو قاموس غير مرتب ‎std::unordered_map‎، والذي يقبل أيّ كائن نمطي كمفتاح، وستكون قيم القاموس كائنات قابلة للنسخ. مثلًا:

std::map<regular_type, std::any>

وعلى عكس ‎any‎، فلا يحسِّن النوع النمطي ‎regular_type‎ الذي أنشأناه في المثال أعلاه الكائنات الصغيرة (small object optimization)، ولا يدعم استعادة البيانات الأصلية، لكنّ ليس من الصعب الحصول على النوع الأصلي على أيّ حال.

يتطّلب تحسين الكائنات الصغيرة حفظ مخزن مؤقّت مُحاذَى (aligned storage buffer) داخل ‎regular_type‎، وتعديل الحاذف ‎ptr‎ بحذر كي ندمر الكائن دون حذفه. وسنبدأ من ‎make_dtor_unique_ptr‎، ونعلّمه كيفيّة تخزين البيانات في المخزن المؤقّت، ثمّ في الكومة (heap) إذا لم يكن هناك مُتّسع في المخزن المؤقّت.

الآلية الأساسية

شطب النوع طريقةٌ لإخفاء نوع الكائن عن الشيفرة التي تستخدمه، حتى لو لم يكن مشتقًّا من أحد الأصناف الأساسية الشائعة، ويوفّر جسرًا بين عوالم تعددية الأشكال الساكنة (static polymorphism)، إذ يجب أن يكون النوع معروفًا بشكل تام عند استخدام القوالب في وقت التصريف، لكن لا يلزم أن يكون مُصرّحًا ليتوافق مع واجهة معيّنة عند التعريف، وبين تعددية الأشكال الديناميكية، إذ لا يلزم أن يكون النوع معروفًا بشكل كامل في وقت التصريف عند استخدام الوراثة والدوال الوهميّة، لكن يجب التصريح بأنّه يتوافق مع واجهة معيّنة عند التعريف.

توضّح الشيفرة التالية الآلية الأساسية لشطب النوع.

#include <ostream>

    class Printable
    {
        public:
            template < typename T>
            Printable(T value): pValue(new Value<T> (value)) {}
    ~Printable()
            {
                delete pValue;
            }

        void print(std::ostream &os) const
        {
            pValue->print(os);
        }

        private:
            Printable(Printable
                const &) /*in C++1x: =delete */ ;    // not implemented غير منفَّذ
        void operator=(Printable
            const &) /*in C++1x: =delete */ ;    //  غير منفَّذ
        struct ValueBase
        {
            virtual~ValueBase() = default;
            virtual void print(std::ostream &) const = 0;
        };
        template < typename T>
            struct Value: ValueBase
            {
                Value(T
                    const &t): v(t) {}

                virtual void print(std::ostream &os) const
                {
                    os << v;
                }

                T v;
            };
        ValueBase * pValue;
    };

وحده التعريف أعلاه من يلزم أن يكون مرئيًا في موقع الاستخدام، تمامًا كما في الأصناف الأساسية ذات الدوال الوهميّة. مثلا:

#include <iostream>

void print_value(Printable const &p)
{
    p.print(std::cout);
}

لاحظ أنّ هذا ليس قالبًا، وإنّما دالة عادية لا يلزم التصريح عنها إلا في ملف الترويسة، ويمكن تعريفها في ملف تنفيذ (implementation file) على عكس القوالب، التي يجب أن يكون تعريفها مرئيًا في مكان الاستخدام. كذلك لا يلزم معرفة أي شيء عن ‎Printable‎ في تعريفات الأنواع الحقيقية (concrete types)، باستثناء أن يكون متوافقًا مع الواجهة كما هو الحال مع القوالب:

struct MyType
{
    int i;
};
ostream &operator<<(ostream &os, MyType const &mc)
{
    return os << "MyType {" << mc.i << "}";
}

يمكننا الآن تمرير كائن من هذا الصنف إلى الدالّة المُعرَّفة أعلاه:

MyType foo = { 42 };
print_value(foo);

شطب النوع إلى مخزن مؤقّت متجاور يضمّ عناصر من النوع T

لا يستدعي شطبُ الأنواعِ الوراثةَ الوهمية بالضرورة أو تخصيصات الذاكرة أو حتى مؤشّرات الدوالّ، وما يميّزه أنّه يصف مجموعة من السلوكيات ويأخذ أيّ نوع يدعم تلك السلوكيات ويغلّفه، أمّا السلوكيات الأخرى التي تميّز ذلك النوع غير الموجودة في تلك المجموعة "فتُنسى" أو "تُشطب".

تأخذ ‎array_view‎ مجالًا (range) أو نوع حاوية وتشطب كلّ شيء باستثناء حقيقة أنّه مخزن مؤقّت متجاور يحتوي عناصر من النوع ‎T‎.

// SFINAE سمة مساعدة لقاعدة
template < class T>
    using data_t = decltype(std::declval<T> ().data());
template < class Src, class T>
    using compatible_data = std::integral_constant<bool, std::is_same<data_t<Src>, T*> {} ||
    std::is_same<data_t < Src>, std::remove_const_t<T> *> {} > ;
template < class T>
    struct array_view
    {
        // نواة الصنف
        T *b = nullptr;
        T *e = nullptr;
        T* begin() const
        {
            return b;
        }

        T* end() const
        {
            return e;
        }

        // توفير التوابع المتوقّعة من مجال متجاور
        T* data() const
        {
            return begin();
        }

        bool empty() const
        {
            return begin() == end();
        }

        std::size_t size() const
        {
            return end() - begin();
        }

        T &operator[](std::size_t i) const
        {
            return begin()[i];
        }

        T &front() const
        {
            return* begin();
        }

        T &back() const
        {
            return *(end() - 1);
        }

        // مساعدات مفيدة لتوليد مجالات أخرى من هذا المجال بشكل سريع وآمن
        array_view without_front(std::size_t i = 1) const
        {
            i = (std::min)(i, size());
            return {
                begin() + i, end()
            };
        }

        array_view without_back(std::size_t i = 1) const
        {
            i = (std::min)(i, size());
            return {
                begin(), end() - i
            };
        }

array_view هو منسق بيانات بصيغة البيانات القديمة، لذا النسخة الافتراضية: …

        array_view(array_view
                const &) = default;
        // empty range توليد مجال فارغ
        array_view() = default;
        // المنشئ النهائي
        array_view(T *s, T *f): b(s), e(f) {}

        array_view(T *s, std::size_t length): array_view(s, s + length) {}

منشئ sfinae، يأخذ أي حاوية تدعم ()data. أو مدىً (range) آخَر في خطوة واحدة. …

        template < class Src,
            std::enable_if_t<compatible_data<std::remove_reference_t<Src> &, T> {}, int>* = nullptr,
            std::enable_if_t<!std::is_same<std::decay_t < Src>, array_view> {}, int>* = nullptr >
            array_view(Src && src):
            array_view(src.data(), src.size()) {}

        // منشئ مصفوفات
        template<std::size_t N>
            array_view(T(&arr)[N]): array_view(arr, N) {}

        // قائمة مهيئ
        template < class U,
            std::enable_if_t<std::is_same<const U, T> {}, int>* = nullptr >
                array_view(std::initializer_list<U> il): array_view(il.begin(), il.end()) {}
    };

تأخذ ‎array_view‎ أيّ حاوية تدعم تابعَ ‎.data()‎ يعيد مؤشّرًا إلى النوع ‎T‎، وتابعَ ‎.size()‎ آخر أو مصفوفة، ثم تشطبها لتصبح مجالًا عشوائيّ الوصول (random-access range) إلى عناصر متجاورة من النوع ‎T‎.

‎array_view‎ يمكن أن تأخذ ‎std::vector<T>‎ أو ‎std::string<T>‎ أو ‎std::array<T, N>‎ أو ‎T[37]‎ أو قائمة مهيئ (initializer list)، بما في ذلك تلك القوائم المبنية بـ ‎{}‎، أو أيّ شيءٍ آخر يدعمها (عبر ‎T* x.data()‎ و ‎size_t x.size()‎).

هنا في هذه الحالة، ستعني البيانات التي نستطيع استخراجها من الشيء الذي نشطبه إضافة إلى الحالة غير المالكة (non-owning state) الخاصة بنا أننا لسنا مضطرين إلى تخصيص ذاكرة أو كتابة دوال مخصّصة تعتمد على النوع.

انظر هذا المثال الحيّ للتوضيح.

قد يكون أحد التحسينات الممكنة هو استخدام ‎data‎ و ‎size‎ غير أعضاء (non-member) في سياق تمكين البحث القائم على الوسائط (ADL).

شطب النوع عبر std::any

يستخدم هذا المثال C++‎ 14 و ‎boost::any‎. أمّا في C++‎ 17، فيمكنك استخدام ‎std::any‎ بدلاً من ذلك.

const auto print =
    make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });
super_any < decltype(print) > a = 7;
(a->*print)(std::cout);

يعتمد هذا المثال على العمل الذي قام به ‎‎@dyp و ‎‎@cpplearner، مع مساهمةٍ من آدم نيفراومونت. سنستخدم أولاً وسمًا لتمرير الأنواع:

template < class T > struct tag_t
{
    constexpr tag_t() {};
};
template < class T > constexpr tag_t<T> tag {};

يحصل هذا الصنف على البصمة (signature) المُخزّنة باستخدام ‎any_method‎، وينشئ هذا نوع مؤشّر دالة (function pointer type)، ومُنتِجًا - factory - لمؤشّرات الدوال المذكورة:

template<class any_method>
using any_sig_from_method = typename any_method::signature;
template<class any_method, class Sig=any_sig_from_method<any_method>>
struct any_method_function;
template<class any_method, class R, class...Args>
struct any_method_function<any_method, R(Args...)>
    {
        template < class T>
        using decorate = std::conditional_t< any_method::is_const, T const, T >;

        using any = decorate<boost::any > ;

       using type = R(*)(any&, any_method const*, Args&&...);
        template < class T>
        type operator()(tag_t < T>) const
        {
           return +[](any& self, any_method const* method, Args&&...args) {
               return (*method)( boost::any_cast<decorate<T>&>(self), decltype(args)(args)... );
            };
        }
    };
  • any_method_function::type - يمثّل نوع مؤشّر الدالّة الذي سنخزّنه بجانب النُسخة.
  • any_method_function::operator()‎ - يأخذ tag_t<T>‎ ويكتب نُسخةً مخصّصة من النوع any_method_function::type، والتي تفترض أنّ ‎any‎ سيساوي ‎T‎.

نحن نريد أن نشطب نوع عدّة توابع في الوقت نفسه، لذا سنجمعها في صفوف (tuples) ونكتب مغلِّفًا (wrapper) مساعدًا لتثبيت الصفّ في موقع تخزين ساكن (static storage) لكل نوع على حدة، مع الحفاظ على مؤشّر يشير إليها.

template<class...any_methods>
using any_method_tuple = std::tuple< typename any_method_function<any_methods>::type... >;
template<class...any_methods, class T>
    any_method_tuple<any_methods...> make_vtable( tag_t<T> ) {
        return std::make_tuple(
            any_method_function<any_methods>{}(tag<T>)...
    );
    }

template < class...methods >
    struct any_methods
    {
        private: any_method_tuple < methods... > const *vtable = 0;
        template < class T>
      static any_method_tuple<methods...> const* get_vtable( tag_t<T> ) {
           static const auto table = make_vtable<methods...>(tag<T>);
            return &table;
        }

        public: any_methods() = default;
        template < class T>
        any_methods(tag_t < T>): vtable(get_vtable(tag < T>)) {}

        any_methods& operator=(any_methods const&)=default;
        template < class T>
       void change_type( tag_t<T> ={} ) { vtable = get_vtable(tag<T>); }

        template < class any_method>
        auto get_invoker(tag_t<any_method> = {}) const
        {
            return std::get < typename any_method_function<any_method>::type > (*vtable);
        }
    };

يمكننا تخصيص هذا للحالات التي يكون فيها الجدول الوهمي vtable صغيرًا، على سبيل المثال إن كان مؤلفًا من عنصر واحد، واستخدام المؤشّرات المباشرة المخزّنة في الصنف في تلك الحالات لتحسين الكفاءة.

سنستخدم ‎super_any_t‎ لتيسير تصريح ‎super_any‎.

template < class...methods >
    struct super_any_t;

يبحث هذا في التوابع التي يدعمها super_any لأجل تطبيق قاعدة "فشل التعويض ليس خطأ أو (SFINAE)" وكذلك تحسين رسائل الخطأ:

template<class super_any, class method>
struct super_method_applies_helper : std::false_type {};
template<class M0, class...Methods, class method>
struct super_method_applies_helper<super_any_t<M0, Methods...>, method> :
std::integral_constant<bool, std::is_same<M0, method>{} ||
super_method_applies_helper<super_any_t<Methods...>, method>{}>
{};
template<class...methods, class method>
auto super_method_test( super_any_t<methods...> const&, tag_t<method> )
{
return std::integral_constant<bool, super_method_applies_helper< super_any_t<methods...>, method
>{} && method::is_const >{};
}
template<class...methods, class method>
auto super_method_test( super_any_t<methods...>&, tag_t<method> )
{
return std::integral_constant<bool, super_method_applies_helper< super_any_t<methods...>, method
>{} >{};
}
template<class super_any, class method>
struct super_method_applies:
decltype( super_method_test( std::declval<super_any>(), tag<method> ) )
{};

بعد ذلك سننشئ النوع ‎any_method‎، وهو عبارة عن مؤشّر إلى تابع زائف (pseudo-method-pointer). سنجعله عامًّا (globally) و ثابتًا (‎const‎) باستخدام الصياغة التالية:

const auto print = make_any_method([](auto && self, auto && os)
{
    os << self;
    });

و في C++‎ 17:

const any_method print =[](auto && self, auto && os)
{
    os << self;
};

لاحظ أنّ عدم استخدام تعابير لامدا قد يتسبّب في فوضى إذ أنّنا نستخدم النوع في البحث، يمكن إصلاح هذا الخلل، لكنّ ذلك سيجعل هذا المثال أطول ممّا هو عليه. وبشكل عام، يجب أن تهيئ توابع any بواسطة تعبير لامدا، أو صنف ذي معامِلات غير محددة النوع (Parameterised type) في لامدا.

template < class Sig, bool const_method, class F>
    struct any_method
    {
        using signature = Sig;
        enum
        {
            is_const = const_method
        };
        private:
            F f;
        public:
            template < class Any,
            // تطابق هذا النوع Anys من أنّ أحد كائنات SFINAE تتحقق قاعدة
            std::enable_if_t<super_method_applies < Any &&, any_method> {}, int>* = nullptr >
            friend auto operator->*( Any&& self, any_method const& m ) {

لا تستخدم قيمة any_method إذ لكل تابع any_method نوعًا خاصًا، وتحقق أن لكل عنصر *auto في super_any مؤشرًا يشير إليك. ثم أرسل إلى any_method_data، تابع …

m](auto&&...args)->decltype(auto)
                {
                    return invoke( decltype(self)(self), &m, decltype(args)(args)... );
                };
            }

        any_method(F fin): f(std::move(fin)) {}

        template < class...Args >
            decltype(auto) operator()(Args&&...args)const 
            {
                return f(std::forward<Args> (args)...);
            }
    };

هذا تابعٌ منتِج (factory method)، لكني لا أراه ضروريًا في C++‎ 17:

template<class Sig, bool is_const=false, class F>
any_method<Sig, is_const, std::decay_t<F>>
make_any_method( F&& f ) {
return {std::forward<F>(f)};
}

هذه هي ‎any‎ المُعزّزة، فهي من النوع ‎any‎، وتحمل معها حزمة من مؤشّرات دوال شطب النوع (type-erasure function pointers) التي تتغيّر كلما تغيّرت ‎any‎ المحتواة:

template < class...methods >
    struct super_any_t: boost::any, any_methods < methods... >
    {
        using vtable = any_methods < methods... > ;
        public: template < class T,
        std::enable_if_t< !std::is_base_of<super_any_t, std::decay_t<T>>{}, int> =0 >
        super_any_t( T&& t ): boost::any(std::forward<T> (t))
        {
            using dT = std::decay_t<T> ;
            this->change_type(tag < dT>);
        }

        boost::any& as_any()&{return *this;}
        boost::any&& as_any()&&{return std::move(*this);}
        boost::any const& as_any()const&{return *this;}

        super_any_t() = default;
        super_any_t(super_any_t&& o): boost::any(std::move(o.as_any())),
        vtable(o) {}

        super_any_t(super_any_t
            const& o): boost::any(o.as_any()),
        vtable(o) {}

        template < class S,
        std::enable_if_t<std::is_same<std::decay_t<S>, super_any_t> {}, int> = 0 >
        super_any_t( S&& o ): boost::any(std::forward<S> (o).as_any()),
        vtable(o) {}

        super_any_t& operator=(super_any_t&&) = default;
        super_any_t& operator=(super_any_t
            const &) = default;

        template < class T,
       std::enable_if_t< !std::is_same<std::decay_t<T>, super_any_t>{}, int>* =nullptr     >
        super_any_t& operator=( T&& t ) {
     ((boost::any&)*this) = std::forward<T>(t);
using dT=std::decay_t<T>;
this->change_type( tag<dT> );
return *this;
}
};

تخزين التوابع ‎any_method‎ ككائنات ثابتة (‎const‎) يُسهّل ‎super_any‎ بعض الشيء:

template<class...Ts>
using super_any = super_any_t< std::remove_cv_t<Ts>... >;

شيفرة تختبر ما سبق:

const auto print = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });
const auto wprint = make_any_method<void(std::wostream&)>([](auto&& p, std::wostream& os ){ os << p << L"\n"; });

int main()
{
    super_any < decltype(print), decltype(wprint) > a = 7;
    super_any < decltype(print), decltype(wprint) > a2 = 7;
    (a->*print)(std::cout);
    (a->*wprint)(std::wcout);
}

هذا مثال حيّ على ذلك.

الأنواع الذرية Atomic Types

يمكن استخدام الأنواع الذرية للقراءة والكتابة بأمان في موضع من الذاكرة مشترك بين خيطين، وهذا ما يسمى الوصول متعدّد الخيوط Multi-threaded Access. انظر المثال التالي على نموذج سيء ويحتمل أن يتسبب في مشكلة سباق بيانات (Data Race)، ستضيف الدالة كل القيم الموجودة بين a و b إلى result:

#include <thread>
#include <iostream>

void add(int a, int b, int *result)
{
    for (int i = a; i <= b; i++)
    {
        *result += i;
    }
}

int main()
{
    //  نوع بيانات أولي غير مؤمَّن خيطيًا
    int shared = 0;

هنا، أنشئ خيطًا يمكنه العمل بشكل موازي للخيط الرئيسي، لينفّذ دالة add المعرَّفة أعلاه بحيث يكون معامِل a يساوي 1، وb يساوي 100، والنتيجة result تساوي shared&. هذا مماثل لـ (;add, 1, 100, &shared) انظر …

     std::thread addingThread(add, 1, 100, &shared);

حاول طباعة قيمة shared إلى الشاشة، سيكرر الخيط الرئيسي ذلك إلى أن يصبح addingThread قابلًا للضم، انظر…

    while (!addingThread.joinable())
    {

قد يتسبب هذا في سلوك غير محدد أو في طباعة قيمة غير صالحة إن حاول addingThread كتابة shared أثناء قراءة الخيط الرئيسي لها، …

        std::cout << shared << std::endl;
    }

    // إعادة ضم الخيط عند نهاية التنفيذ لأغراض التنظيف
    addingThread.join();

    return 0;
}

قد يتسبب المثال أعلاه في قراءة خاطئة وقد يؤدّي إلى سلوك غير مُحدّد. انظر المثال التالي على استخدام آمن للخيوط (thread safety)، ستضيف الدالة كل القيم الواقعة بين a و b إلى result:

#include <atomic>
#include <thread>
#include <iostream>

void add(int a, int b, std::atomic<int> *result)
{
    for (int i = a; i <= b; i++)
    {
        // تلقائيا result إلى 'i' إضافة
        result->fetch_add(i);
    }
}

int main()
{
    // استخدام قالب ذري لتخزين كائنات غير ذرية
    std::atomic<int> shared = 0;

هنا، أنشئ خيطًا يمكنه العمل بشكل موازي للخيط الرئيسي، لينفّذ دالة add المعرَّفة أعلاه بحيث يكون معامِل a يساوي 1، وb يساوي 100، والنتيجة result تساوي shared&. هذا مماثل لـ (;add, 1, 100, &shared) انظر …

    std::thread addingThread(add, 1, 100, &shared);

حاول طباعة قيمة shared إلى الشاشة، سيكرر الخيط الرئيسي ذلك إلى أن يصبح addingThread قابلًا للضم، انظر…

    while (!addingThread.joinable())
    {
    // طريقة آمنة لقراءة القيمة المشارَكة من أجل قراءة آمنة خيطيًا.
        std::cout << shared.load() << std::endl;
    }

    // إعادة ضم الخيط عند نهاية التنفيذ لأغراض التنظيف
    addingThread.join();

    return 0;
}

المثال أعلاه آمن لأنّ جميع عمليات ‎store()‎ و ‎load()‎ الخاصّة بالبيانات الذرية (‎atomic‎) تحمي العدد ‎int‎ المغلّف من محاولات الوصول الآني (simultaneous access).

تحويلات النوع الصريحة Explicit type conversions

يمكن تحويل تعبير بشكل صريح إلى نوع ‎T‎ باستخدام ‎dynamic_cast<T>‎ أو ‎static_cast<T>‎ أو ‎reinterpret_cast‎ أو ‎const_cast‎ ، وذلك اعتمادًا على النوع الذي تريد التحويل إليه.

كذلك تدعم C++‎ صيغة التحويل الدالّية - function-style cast -‏ ‎T(expr)‎، وصيغة التحويل على نمط C ‏‎(T)expr‎.

التحويل على نمط C

سُمّي كذلك لأنّه التحويل الوحيد الذي يمكن استخدامه في C. وصياغته على الشكل التالي ‎(NewType)variable‎. يستخدم هذا التحويل أحد تحويلات C++‎ التالية (بالترتيب):

  • const_cast<NewType>(variable)‎
  • static_cast<NewType>(variable)‎
  • const_cast<NewType>(static_cast<const NewType>(variable))‎
  • reinterpret_cast<const NewType>(variable)‎
  • const_cast<NewType>(reinterpret_cast<const NewType>(variable))‎

صيغة التحويل الدوالي مشابهة جدًا رغم وجود بعض القيود الناتجة عن الصيغة: ‎NewType(expression)‎، ونتيجة لذلك لا يمكن التحويل إلّا إلى الأنواع التي لا تحتوي على مسافات فارغة.

يُفضّل استخدام تحويل C++‎ الجديد لأنّه أسهل قراءةً ويمكن رصده بسهولة داخل شيفرة C++‎ المصدرية، كما يمكن رصَد الأخطاء في وقت التصريف وليس في وقت التشغيل. ونظرًا لأنّ هذا التحويل قد ينتج عنه ‎reinterpret_cast‎ غير مقصود، فإنّه غالبًا ما يُعدُّ خطيرًا.

التخلّص من الثباتية Casting away constness

يمكن تحويل مؤشّر-إلى-كائن-ثابت إلى مؤشّر-إلى-كائن-غير-ثابت باستخدام الكلمة المفتاحية ‎const_cast‎، وسنستخدمها لاستدعاء دالّة غير صحيحة ثباتيًا (const-correct) ولا تقبل إلّا وسيطا غير ثابت من النوع ‎char*‎، رغم أنّها لا تكتُب في المؤشّر أبدًا:

void bad_strlen(char*);
const char *s = "hello, world!";
bad_strlen(s);    // خطأ تصريفي
bad_strlen(const_cast<char*> (s));   

في الشيفرة السابقة، يفضل جعل bad_strlen تقبل معامِل *const char. يمكن أن يُستخدم إشارة ‎const_cast‎ إلى نوع معيّن في تحويل قيمة يسارية مُؤهّلة ثباتيًا (const-qualified) إلى قيمة يمينية غير مؤهّلة ثباتيًا. لكن يكتنف استخدام‎const_cast‎ بعض الخطورة لأنّه يجعل من المستحيل على نظام الأنواع في C++‎ أن يمنعك من محاولة تعديل كائن ثابت. وهو أمر سيؤدّي إلى سلوك غير مُحدّد.

const int x = 123;
int& mutable_x = const_cast< int&> (x);
mutable_x = 456;    // قد يُصرَّف، لكنه قد يُنتِجُ سلوكًا غير محدد

التحويل من صنف أساسي إلى صنف مشتق منه

يمكن تحويل مؤشّر لصنف أساسي إلى مؤشّر لصنف مشتق منه باستخدام ‎static_cast‎، ولا تُجري ‎static_cast‎ أيّ تحقّق في وقت التشغيل، لذلك قد يحدث سلوك غير مُحدّد في حال كان المؤشّر يشير إلى نوعٍ غير النوع المطلوب.

struct Base {};
struct Derived : Base {};
Derived d;
Base* p1 = &d;
Derived* p2 = p1;   // خطأ، لا بد من التحويل
Derived* p3 = static_cast<Derived*>(p1);  // يشير الآن إلى الكائن المشتق p2 لا بأس فـ
Base b;
Base* p4 = &b;
Derived* p5 = static_cast<Derived*>(p4);  //  p4 سلوك غير محد، لأن
// لا يشير إلى كائن مشتق  

وبالمثل، يمكن تحويل مرجع يشير إلى الصنف الأساسي إلى مرجع يشير إلى صنف مشتق باستخدام ‎static_cast‎.

struct Base {};
struct Derived : Base {};
Derived d;
Base& r1 = d;
Derived& r2 = r1;   // خطأ، لا بدّ من التحويل
Derived& r3 = static_cast<Derived&>(r1);   // يشير الآن إلى الكائن المشتق p3 لا بأس فـ

إذا كان النوع المصدري متعدد الأشكال فيمكن استخدام ‎dynamic_cast‎ للتحويل من الصنف الأساسي إلى صنف مشتقّ منه، إذ أنّها تُجري فحصًا في وقت التشغيل، ويمكن معالجة الفشل هذه المرّة بدلاً من حدوث سلوك غير محدّد. أما إن كان مؤشّرًا، سيُعاد مؤشّر فارغ عند الفشل. وفي حال كان مرجعًا، سيُطرَح استثناء عند فشل ‎std::bad_cast‎ (أو صنف مشتق من ‎std::bad_cast‎).

struct Base
{
    virtual~Base();
};    // هي بنية متعدد الأشكالBase 
struct Derived: Base {};
Base* b1 = new Derived;
Derived* d1 = dynamic_cast<Derived*>(b1);     // يشير الآن إلى الكائن المشتق p1 لا بأس فـ
Base* b2 = new Base;
Derived* d2 = dynamic_cast<Derived*>(b2);  // هو مؤشر فارغ d2

التحويل بين المؤشرات والأعداد الصحيحة

يمكن تحويل مؤشّر كائن (بما في ذلك ‎void*‎) أو مؤشّر دالّة إلى نوع عددي صحيح باستخدام ‎reinterpret_cast‎، ولن يُصرَّف هذا إلّا إذا كان النوع المقصود طويلاً بما فيه الكفاية. تتعلّق النتيجة بالتنفيذ، وتعيد في العادةً العنوان العددي للبايت الذي يشير إليه المؤشّر في الذاكرة.

وعمومًا فالنوعين ‎long‎ و ‎unsigned long‎ طويلان بما يكفي لحفظ أيّ قيمة للمؤشّر، لكنّ هذا غير مضمون من قبل المعيار.

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

في حال وجود النوعين ‎std::intptr_t‎ و ‎std::uintptr_t‎ فإننا نضمن أن يكفي طولهما لاحتواء ‎void*‎ (ومن ثم أيّ مؤشّر إلى نوع كائن)، لكن لا نضمن أن يكفي طولهما لاحتواء مؤشّر دالّة.

وبالمثل، يمكن استخدام ‎reinterpret_cast‎ لتحويل نوع عددي صحيح إلى نوع مؤشّر. ومرّة أخرى، فالنتيجة تتعلّق بالتنفيذ، غير أنّه يُضمَن ألّا يتم تغيير قيمة المؤشّر عن طريق تقريب (round-trip) نوع عددي صحيح، كما لا يضمن المعيار أنّ القيمة صفر ستُحوّل إلى مؤشّر فارغ.

void register_callback(void (*fp)(void*), void* arg); 
  // C على الأرجح واجهة برمجة تطبيقات للغة 
void my_callback(void* x) 
{
    std::cout << "the value is: " <<reinterpret_cast<long> (x);    // ستُصرَّف على الأرجح
}

long x;
std::cin >> x;
register_callback(my_callback,
    reinterpret_cast<void*> (x));    // نأمل عدم فقدان أيّ معلومات

التحويل عبر مُنشئ صريح أو دالة تحويل صريحة

التحويلات التي تشمل استدعاء مُنشئ صريح أو دالّة تحويل لا يمكن إجراؤها ضمنيًا، لكن نستطيع طلب إجراء التحويل بشكل صريح باستخدام ‎static_cast‎، ذلك يشبه التهيئة المباشرة (direct initialization) باستثناء أنّ النتيجة تكون مؤقّتة.

class C
{
    std::unique_ptr<int> p;
    public:
        explicit C(int* p): p(p) {}
};
void f(C c);
void g(int* p)
{
    f(p);    // error: C::C(int*) is explicit
    f(static_cast<C> (p));    // ok
    f(C(p));    // يكافئ السطر الماضي
    C c(p);
    f(c);    // error: C is not copyable
}

التحويل الضمني Implicit conversion

تستطيع أن تُجري ‎static_cast‎ أيّ تحويل ضمني، وقد يكون هذا مفيدًا أحيانًا كما في الأمثلة التالية:

  • عند تمرير الوسائط إلى علامة حذف (ellipsis)، لن يكون نوع الوسيط "المُتوقَّع" معروفًا بشكل ساكن (statically known)، لذا لن يحدث أي تحويل ضمني.
const double x = 3.14;
printf("%d\n", static_cast<int> (x));    // 3
// printf("%d\n", x);    // تتوقع عددا صحيحا هنا printf سلوك غير محد، لأنّ
//  حل بديل 
// const int y = x; printf("%d\n", y);

بدون التحويل الصريح للنوع، سيُمرَّر كائن من النوع ‎double‎ إلى علامة الحذف وسيحدث سلوك غير مُحدّد.

  • يمكن لمُعامل الإسناد الخاصّ بصنف مشتقّ أن يستدعي مُعامل الإسناد الخاصّ بصنفه الأساسي على النحو التالي:
struct Base
{ /*... */ };
struct Derived: Base
{
    Derived& operator=(const Derived& other) 
    {
        static_cast<Base&>(*this) = other;
        // :حل بديل
        // Base& this_base_ref = *this; this_base_ref = other;
    }
};

تحويل التعدادات Enum conversions

يمكن أن تجري ‎static_cast‎ عمليّة تحويل من نوع عددي صحيح أو عشري إلى نوع تعدادي (سواء كان نطاقًيا - scoped - أو غير نطاقي - unscoped) والعكس صحيح، كما أنّها تحوّل بين أنواع التعدادات.

*التحويل من نوع تعداد غير نطاقي إلى نوع حسابي (arithmetic type) يُعدُّ تحويلًا ضمنيًا؛ وهو جائز لكنه غير ضروري لاستخدام ‎static_cast‎.

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

  • عند تحويل نوع تعداد نطاقي إلى نوع حسابي:
  • إذا كان من الممكن تمثيل قيمة التعداد في النّوع المقصود بشكل تامّ، فإنّ النتيجة ستساوي تلك القيمة.
  • وإلا، ستكون النتيجة غير مُحدّدة إن كان النوع المقصود نوعًا صحيحًا.
  • بخلاف ذلك، إذا كان النوع المقصود نوعًا عشريا (floating point type)، فإنّ النتيجة ستساوي نتيجة التحويل إلى النوع الأساسي، ثم منه إلى نوع الأعداد العشرية. انظر:
enum class Format
{
    TEXT = 0,
        PDF = 1000,
        OTHER = 2000,
};
Format f = Format::PDF;
int a = f;    // error
int b = static_cast<int> (f);    // يساوي 1000 b 
char c = static_cast<char> (f);    //  char غير محدد في حال لم يتناسب 1000 مع النوع
double d = static_cast<double> (f);    // يساوي على الأرجح 1000.0 d  
  • عند تحويل عدد صحيح أو نوع تعداد إلى نوع تعداد:
  • إذا كانت القيمة الأصلية ضمن مجال (range) التعداد المقصود، فإنّ النتيجة ستساوي تلك القيمة. لاحظ أنّ هذه القيمة قد تختلف من عدّاد (enumerator) لآخر.
  • وإلا، فإنّ النتيجة ستكون غير محددة (unspecified) في ‏(C++‎ 14<=)، أو غير معرَّفة (undefined) في ‏(C++‎ 17>=). انظر المثال التالي:
enum Scale
{
    SINGLE = 1,
        DOUBLE = 2,
        QUAD = 4
};
Scale s1 = 1;    // خطأ
Scale s2 = static_cast<Scale> (2);    // DOUBLE من النوع s2 
Scale s3 = static_cast<Scale> (3);    // تساوي 3، وهي غير مساوية لأيّ عدّاد s3 قيمة
Scale s9 = static_cast<Scale> (9);    // C++17 سلوك غير محدد في
//  C++14 وقيمة غير موصوفة في

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

  • عند تحويل نوع عشري إلى نوع تعداد، فإنّ النتيجة ستكون مساوية لنتيجة التحويل إلى النوع الأساسي للتعداد ثمّ إلى نوع التعداد.
enum Direction
{
    UP = 0,
        LEFT = 1,
        DOWN = 2,
        RIGHT = 3,
};
Direction d = static_cast<Direction> (3.14);   

تحويل المؤشّرات العضوية

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

ولن يكون التحويل صالحًا خلاف ذلك، إلّا إذا كان العضو المُشار إليه عبر المعامَل موجودًا بالفعل في الصنف المقصود، أو كان الصنف المقصود صنفًا أساسيًا أو مشتقًّا من الصنف الذي يحتوي على العضو الذي يشير إليه المعامَل، إذ لا تتحقّق ‎static_cast‎ من الصحّة (validity). ولن يكون السلوك الناتج محددًا إن لم يكن التحويلُ صالحًا.

struct A {};
struct B
{
    int x;
};
struct C: A, B
{
    int y;
    double z;
};
int B::*p1 = &B::x;
int C::*p2 = p1;    // حسنا، تحويل ضمني
int B::*p3 = p2;    // خطأ
int B::*p4 = static_cast<int B::*>(p2);   // p1 يساوي p4  حسنا

ما يلي غير معرَّف، إذ يشير p2 إلى x وهو عضو من الصنف B وهو غير ذي صلة هنا، نتابع ….

int A::*p5 = static_cast<int A::*>(p2);          
double C::*p6 = &C::z;
double A::*p7 = static_cast<double A::*>(p6);    // z لا تحتوي A لا بأس، رغم أنّ
int A::*p8 = static_cast<int A::*>(p6);   // error: types don't match

التحويل من *void‎‎ إلى *T‎‎

في C++‎، لا يمكن تحويل ‎void*‎ ضمنيًا إلى ‎T*‎ إن كان ‎T‎ يمثّل نوعَ كائن، بل يجب استخدام ‎static_cast‎ لإجراء التحويل بشكل صريح. وإذا كان العامل يشير فعليًا إلى كائن ‎T‎، فإنّ النتيجة ستشير إلى ذلك الكائن، وإلا فإنّ النتيجة ستكون غير مُحدّدة.

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

وحتى لو لم يكن العامل يشير إلى كائن ‎T‎، فما دام المعامَل يشير إلى بايت تمت محاذاة عنوانه بشكل صحيح مع النوع ‎T‎، فإنّ نتيجة التحويل ستشير إلى نفس البايت.

// تخصيص مصفوفة من 100 عدد صحيح بالطريقة الصعبة.
int* a = malloc(100*sizeof(*a)); // error; malloc returns void*
int* a = static_cast<int*>(malloc(100*sizeof(*a)));    // حسنا
// int* a = new int[100];                            // لا حاجة للتحويل
// std::vector<int> a(100);                            // هذا أفضل
const char c = '!';
const void* p1 = &c;
const char* p2 = p1;    // خطأ
const char* p3 = static_cast<const char*>(p1);   // c يشير إلى p3 حسنا
const int* p4 = static_cast<const int*>(p1); 

السطر السابق غير معرَّف في C++03، وقد يكون غير معرَّف في C++11 أيضًا إن كان (alignof(int أكبر من (alignof(int، بقية المثال …

char* p5 = static_cast<char*>(p1);    // error: casting away constness

تحويل مواراة النوع Type punning conversion

يمكن تحويل مؤشّر (مرجع) يشير إلى نوع كائن، إلى مؤشّر (مرجع) يشير إلى أيّ نوع كائن آخر باستخدام ‎reinterpret_cast‎ دون الحاجة إلى أيّ منشِئات أو دوالّ تحويل.

int x = 42;
char* p = static_cast<char*>(&x); // error: static_cast cannot perform this conversion
char* p = reinterpret_cast<char*>(&x); // حسنا
 *p = 'z';    // x ربما يغيّر هذا

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

تمثّل نتيجة ‎reinterpret_cast‎ نفس العنوان الذي يمثّله العامل، شرط أن تتناسب محاذاة العنوان مع النوع المقصود، وتكون النتيجة غير مُحدّدة في خلاف ذلك.

int x = 42;
char& r = reinterpret_cast<char&>(x);
const void* px = &x;
const void* pr = &r;
assert(px == pr);

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

نتيجة ‎reinterpret_cast‎ غير مُحدّدة، إلّا أنّ المؤشّر (المرجع) سيتجاوز التقريب (round trip) من نوع المصدر إلى نوع الوجهة، والعكس صحيح طالما أن متطلّبات محاذاة النوع المقصود ليست أكثر صرامة من نظيرتها في نوع المصدر.

int x = 123;
unsigned int& r1 = reinterpret_cast<unsigned int&>(x);
int& r2 = reinterpret_cast<int&>(r1);
r2 = 456; // 456 إلى x تعيين

لا تغيّر ‎reinterpret_cast‎ العنوانَ في معظم التنفيذات، وهو أمر لم يُعتمد معيارًا حتى الإصدار C++‎ 11.

كذلك يمكن استخدام ‎reinterpret_cast‎ للتحويل من نوع مؤشّر بيانات عضوية (pointer-to-data-member type) إلى آخر، أو من نوع مؤشر إلى دالة تابعة (pointer- to-member-function type) إلى آخر.

يكتنف استخدام ‎reinterpret_cast‎ بعض الخطورة، لأنّ القراءة أو الكتابة عبر مؤشّر أو مرجع تم الحصول عليه باستخدام ‎reinterpret_cast‎ قد يؤدّي إلى حدوث سلوك غير مُحدّد في حال كان نوعي المصدر والوجهة غير مترابطين.

الأنواع غير المسماة Unnamed types

الأصناف غير المسماة Unnamed classes

على عكس الأصناف أو البنيات المُسمّاة، فإن الأصناف والبنيات غير المُسمّاة يجب استنساخها في موضع تعريفها، ولا يمكن أن يكون لها مُنشئات أو مدمّرات.

struct
{
    int foo;
    double bar;
}

foobar;
foobar.foo = 5;
foobar.bar = 4.0;
class
{
    int baz;
    public:
        int buzz;

    void setBaz(int v)
    {
        baz = v;
    }
}

barbar;
barbar.setBaz(15);
barbar.buzz = 2;

استخدام أنواع الأصناف ككُنى للنوع (type aliases)

يمكن أيضًا استخدام أنواع الأصناف غير المسماة عند إنشاء كُنى (alias) للنوع عبر ‎typedef‎ و ‎using‎:

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

using vec2d = struct
{
    float x;
    float y;
};
typedef struct
{
    float x;
    float y;
}

vec2d;
vec2d pt;
pt.x = 4. f;
pt.y = 3. f;

الأعضاء المجاهيل Anonymous members

تسمح المصرّفات الشهيرة باستخدام الأصناف كأعضاء مجهولين، لكن هذا غير قياسي في لغة C++‎.

struct Example
{
    struct
    {
        int inner_b;
    };

    int outer_b;

    // يمكن الوصول إلى أعضاء الهيكل المجهولة كما لو كانت أعضاءً من الهيكل الأب 
    Example(): inner_b(2), outer_b(4)
    {
        inner_b = outer_b + 2;
    }
};
Example ex;
// نفس الشيء بالنسبة للشيفرات الخارجية التي تشير إلى البنية
ex.inner_b -= ex.outer_b;

الاتحادات المجهولة Anonymous Unions

تنتمي أسماء الأعضاء الخاصة باتحاد مجهول (anonymous union) إلى نطاق تصريح الاتّحاد، ويجب أن تكون مميّزة عن جميع الأسماء الأخرى الموجودة في ذلك النطاق. يستخدم المثال التالي نفس الإنشاء (Construction) الذي استخدمناه في مثال الأعضاء المجهولين أعلاه، ولكنّه هذه المرة متوافق مع المعايير.

struct Sample
{
    union
    {
        int a;
        int b;
    };
    int c;
};
int main()
{
    Sample sa;
    sa.a = 3;
    sa.b = 4;
    sa.c = 5;
}

سمات النوع Type Traits

خاصيات النوع Type Properties

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

تقارن خاصيات النوع المُعدِّلات (modifiers) التي يمكن تطبيقها على عدة متغيّرات، لكنّ فائدة تلك السمات لا تكون واضحة في كل حالة. لاحظ أن التحسين الذي يوفّره المثال التالي لن يكون له أثر إلّا إن لم يكن المُصرّف مُحسّنا من الأساس، فهو مجرّد مثال توضيحي.

template < typename T>
    inline T FastDivideByFour(cont T &var)
    {
        // unsigned integral سيحدث خطأ في حال لم يكن النوع المُدخَل نوع عدديا صحيحا غير مؤشّر  
       static_assert(std::is_unsigned<T>::value && std::is_integral<T>::value,
            "This function is only designed for unsigned integral types.");
        return (var >> 2);
    }

is_const

ستُعاد القيمة true إن كان النوع ثابتًا.

std::cout << std::is_const<const int >::value << "\n";    // true
std::cout <<std::is_const<int>::value << "\n";    // false

is_volatile

ستُعاد القيمة true إن كان النوع متغيّرًا (volatile).

std::cout <<std::is_volatile < static volatile int>::value << "\n";    // true.
std::cout << std::is_const<const int >::value << "\n";    // false.

is_signed

ستُعاد القيمة true إن كان النوع مؤشَّرًا (signed).

std::cout <<std::is_signed<int>::value << "\n";    // true.
std::cout <<std::is_signed<float>::value << "\n";    // true.
std::cout <<std::is_signed < unsigned int>::value << "\n";    // false.
std::cout <<std::is_signed<uint8_t>::value << "\n";    // false.

is_unsigned

ستُعاد القيمة true إن كان النوع غير مؤشّر.

std::cout <<std::is_unsigned < unsigned int>::value << "\n";    // true
std::cout <<std::is_signed<uint8_t>::value << "\n";    // true
std::cout <<std::is_unsigned<int>::value << "\n";    // false 
std::cout <<std::is_signed<float>::value << "\n";    // false

أنواع السمات القياسية Standard type traits

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

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

template < class T > struct is_foo;

عند استنساخ صنف من القالب بواسطة نوع يحقّق معيارًا ‎foo‎، فإنّ ‎is_foo<T>‎ سيرث من ‎std::integral_constant<bool,true>‎ (يُعرَف أيضًا باسم std::true_type)، وإن كان غير ذلك فإنّه يرث من std::integral_constant<bool,false>‎ ( ويعرف أيضًا باسم std::false_type). هذا سيمنح للسمة الأعضاء التالية:

الثوابت

static constexpr bool value

تعيد true إن كان T يحقّق المعيار foo، وتعيد ‎false‎ خلاف ذلك.

الدوال

operator bool

تعيد ‎value‎

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

bool operator()‎

تعيد ‎value‎

الأنواع

الاسم التعريف
value_type bool type std::integral_constant<bool,value>‎

يمكن استخدام السمة في بنيات مثل ‎static_assert‎ أو ‎std::enable_if‎. المثال التالي يستخدم std::is_pointer:

template < typename T>
    void i_require_a_pointer(T t)
    {
        static_assert(std::is_pointer<T>::value, "T must be a pointer type");
    }

// نوعًا مؤشّرًا T التحميل الزائد في حال لم يكن
template < typename T>
    typename std::enable_if<!std::is_pointer<T>::value>::type
does_something_special_with_pointer(T t)
{
    // افعل شيئا عاديا
}

// نوعًا مؤشّرًا T التحميل الزائد في حال لم يكن
template < typename T>
    typename std::enable_if<std::is_pointer<T>::value>::type
does_something_special_with_pointer(T t)
{
    // افعل شيئا خاصا
}

هناك المزيد من السمات التي تُحوّل الأنواع، مثل ‎std::add_pointer‎ و ‎std::underlying_type‎، وتعرض هذه السمات عمومًا نوع عضو يُسمَّى ‎type‎ يحتوي النوعَ المُحوَّل، كما في std::add_pointer<int>::type is int*‎.

std::is_same‎‎

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

تُستخدم دالة العلاقة بين الأنواع - type relation‏ - ‎std::is_same<T, T>‎ لمقارنة نوعين، إذ تعيد true إذا كان النوعان متساويين، و تعيد false خلاف ذلك. في المثال التالي، سيطبع السطر الأول true في أغلب مصرِّفات x86 و x86_64، بينما يطبع السطران الثاني والثالث false في جميع المصرفات.

std::cout <<std::is_same<int, int32_t>::value << "\n";
std::cout <<std::is_same<float, int>::value << "\n";
std::cout <<std::is_same < unsigned int, int>::value << "\n";

تعمل علاقة الأنواع ‎std::is_same‎ بغض النظر عن التعريفات النوعية typedefs، وهو مُبيَّن في المثال الأوّل عند مقارنة ‎int == int32_t‎، لكن ربما لا يكون هذا واضحًا كفاية. انظر المثال التالي إذ سيطبع true في كل المصرفات:

typedef int MyType
std::cout <<std::is_same<int, MyType>::value << "\n";

استخدام std::is_same لإطلاق تحذير عند استخدام صنف أو دالة قالب بطريقة غير صحيحة

يمكن استخدام ‎std::is_same‎ مع static_assert لفرض الاستخدام السليم للأصناف والدوال المُقوْلبة. على سبيل المثال، هذه دالّة لا تسمح إلّا بالمُدخلات من النوع ‎int‎، والاختيار بين بنيتين.

#include <type_traits>
struct foo
{
    int member;
    // متغيّرات أخرى
};

struct bar
{
    char member;
};
template < typename T>
    int AddStructMember(T var1, int var2)
    {

إن كان T != foo || T != bar، اعرض رسالة خطأ، …

        static_assert(std::is_same<T, foo>::value ||
            std::is_same<T, bar>::value,
            "This function does not support the specified type.");
        return var1.member + var2;
    }

سمات النوع الأساسية

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

لدينا في ++C عدة أنواع من السمات تقارن بعض الأنواع العامّة كما سنشرح أدناه:

is_integral

تعيد القيمة true بالنسبة لجميع أنواع الأعداد الصحيحة، مثل ‎int‎ و ‎char‎ و ‎long‎ و ‎unsigned int‎ وغيرها.

std::cout <<std::is_integral<int>::value << "\n";    // true.
std::cout <<std::is_integral<char>::value << "\n";    // true.
std::cout <<std::is_integral<float>::value << "\n";    // false.

is_floating_point

تعيد القيمة true بالنسبة لجميع أنواع الأعداد العشرية، مثل ‎float‎ و ‎double‎ و ‎long double‎ وغيرها

std::cout <<std::is_floating_point<float>::value << "\n";    // true.
std::cout <<std::is_floating_point<double>::value << "\n";    // true.
std::cout <<std::is_floating_point<char>::value << "\n";    // false.

is_enum

تعيد القيمة true بالنسبة لجميع أنواع التعدادات، بما في ذلك ‎enum class‎.

enum fruit
{
    apple, pair, banana
};
enum class vegetable
{
    carrot, spinach, leek
};
std::cout <<std::is_enum<fruit>::value << "\n";    // true.
std::cout <<std::is_enum<vegetable>::value << "\n";    // true.
std::cout <<std::is_enum<int>::value << "\n";    // false.

is_pointer

تعيد القيمة true بالنسبة لجميع المؤشّرات:

std::cout <<std::is_pointer<int*>::value << "\n";    // true.
typedef int *MyPTR;
std::cout <<std::is_pointer<MyPTR>::value << "\n";    // true.
std::cout <<std::is_pointer<int>::value << "\n";    // false.

is_class

تعيد القيمة true بالنسبة لجميع الأصناف والبنيات، باستثناء ‎enum class‎.

struct FOO
{
    int x, y;
};
class BAR
{
    public:
        int x, y;
};
enum class fruit
{
    apple, pair, banana
};
std::cout <<std::is_class<FOO>::value << "\n";    // true.
std::cout <<std::is_class<BAR>::value << "\n";    // true.
std::cout <<std::is_class<fruit>::value << "\n";    // false.
std::cout <<std::is_class<int>::value << "\n";    // false.

تباين النوع المُعاد Return Type Covariance

يُقصَد بتباين نوع القيمة المعادة (Return Type Covariance) من تابع وهمي m السلوك الذي يصبح فيه نوع ذلك التابع (T) أكثر تحديداً عند إعادة تعريف m في صنف مشتق. ونتيجة لذلك، يتغيّر تحديد (specificity ) النوع T كما هو حال الصنف الذي يحتوي التابع m. انظر المثال التوضيحي التالي:

// 2. نسخة النتيجة المتباينة من المثال الأساسي، تحقق النوع الساكن.
class Top
{
    public:
        virtual Top* clone() const = 0;
    virtual~Top() = default;  
};
class D: public Top
{
    public: D* /*← Covariant return */ clone() const override
    {
        return new D(*this);
    }
};
class DD: public D
{
    private: int answer_ = 42;
    public: int answer() const
    {
        return answer_;
    }

    DD * /*← Covariant return */ clone() const override
    {
        return new DD(*this);
    }
};

#include <iostream>
using namespace std;

int main()
{
    DD *p1 = new DD();
    DD *p2 = p1->clone();

تحقق النوع الساكن يضمن النوع الديناميكي الصحيح DD لـ p2*، نتابع المثال …

    cout << p2->answer() << endl;    // "42"
    delete p2;
    delete p1;
}

نتيجة المؤشّرات الذكية المتباينة

إليك المثال التالي:

// 3. (نتيجة تباين المؤشر الذكي (تنظيف آلي.
#include <memory>
using std::unique_ptr;
template < class Type>
    auto up(Type *p)
    {
        return unique_ptr<Type> (p);
    }

class Top
{
    private:
        virtual Top* virtual_clone() const = 0;
    public:
        unique_ptr<Top> clone() const
        {
            return up(virtual_clone());
        }

    virtual~Top() = default; 
};
class D: public Top
{
    private: D * /*← Covariant return */ virtual_clone() const override
    {
        return new D(*this);
    }

    public: unique_ptr<D> /*← Apparent covariant return */ clone() const
    {
        return up(virtual_clone());
    }
};
class DD: public D
{
    private: int answer_ = 42;
    DD * /*← Covariant return */ virtual_clone() const override
    {
        return new DD(*this);
    }

    public: int answer() const
    {
        return answer_;
    }

    unique_ptr<DD> /*← Apparent covariant return */ clone() const
    {
        return up(virtual_clone());
    }
};
#include <iostream>
using namespace std;
int main()
{
    auto p1 = unique_ptr<DD> (new DD());
    auto p2 = p1->clone();

تحقق النوع الساكن يضمن النوع الديناميكي الصحيح DD لـ p2*، نتابع المثال …

    cout << p2->answer() << endl;    // "42"
    //  unique_ptr التنظيف يتم تلقائيا عبر المؤشر الحصري
}

مخطط أنواع الكائنات Layout of object types

أنواع الأصناف

نعني بكلمة "صنف" (class)، أيّ نوع عُرِّف باستخدام الكلمة المفتاحية ‎class‎ أو ‎struct‎ (ولكن ليس بـ ‎enum struct‎ أو ‎enum ‎class‎).

  • حتى إن كان الصنف فارغًا فإنه يحتلّ بايت واحدًا على الأقل في الذاكرة؛ ومن ثم سيتألف من الحشو (padding) فقط، هذا يضمن أنّه إذا أشار مؤشّر ‎p‎ إلى كائن من صنف فارغ، فإنّ ‎p + 1‎ ستكون عنوانًا مختلفًا وستشير إلى كائن مختلف. لكن يمكن أن يساوي حجم صنف فارغ القيمة 0 عند استخدامه كصنف أساسي. راجع: تحسين الأصناف الأساسية الفارغة.
class Empty_1 {};    // sizeof(Empty_1)         == 1
class Empty_2 {};    // sizeof(Empty_2)         == 1
class Derived: Empty_1 {};    // sizeof(Derived)         == 1
class DoubleDerived: Empty_1, Empty_2 {};    // sizeof(DoubleDerived) == 1
class Holder
{
    Empty_1 e;
};    // sizeof(Holder)         == 1
class DoubleHolder
{
    Empty_1 e1;
    Empty_2 e2;
};    // sizeof(DoubleHolder)     == 2
class DerivedHolder: Empty_1
{
    Empty_1 e;
};    // sizeof(DerivedHolder) == 2

التمثيل الخاص بكائن صنف معيّن يحتوي تمثيل كائن الصنف الأساسي، وكذلك أنواع الأعضاء غير الساكنة (non-static member types). على سبيل المثال، في الصنف التالي:

struct S
{
    int x;
    char *y;
};

يوجد تسلسل متتالي حجمه ‎sizeof(int)‎ بايت داخل كائن من النوع ‎S‎، ويُطلق عليه "كائن فرعي" (subobject)، يحتوي قيمة ‎x‎، إضافة إلى كائن فرعي آخر حجمه ‎sizeof(char*)‎ بايت ويحتوي قيمة ‎y‎، ولا يمكن أن يتداخل الاثنان.

  • إذا كان لنوع صنف معيّن أعضاء و/أو أصناف أساسية من الأنواع ‎t1, t2,...tN‎، فينبغي ألّا يقلّ الحجم عن sizeof(t1) + sizeof(t2) + ... + sizeof(tN)‎ نظرًا للنقاط السابقة، لكن بناءً على متطلّبات المحاذاة الخاصّة بالأعضاء والأصناف الأساسية، فقد يضطر المٌصرّف إلى إدراج حشو بين الكائنات الفرعية، أو في بداية الكائن أو نهايته.
struct AnInt
{
    int i;
};
// sizeof(AnInt)        == sizeof(int)
// sizeof(AnInt) == 4 (4) :في أنظمة 32 أو 64 بت
struct TwoInts
{
    int i, j;
};
// sizeof(TwoInts)        >= 2* sizeof(int)
// sizeof(TwoInts)         == 8 (4 + 4)  :في أنظمة 32 أو 64 بت
struct IntAndChar
{
    int i;
    char c;
};
// sizeof(IntAndChar)    >= sizeof(int) + sizeof(char)
// sizeof(IntAndChar)     == 8 (4 + 1 + padding)   :في أنظمة 32 أو 64 بت
struct AnIntDerived: AnInt
{
    long long l;
};
// sizeof(AnIntDerived) >= sizeof(AnInt) + sizeof(long long)
// sizeof(AnIntDerived) == 16 (4 + padding + 8)  :في أنظمة 32 أو 64 بت
  • في حال إدراج الحشو في كائن بسبب متطلّبات المحاذاة، فإنّ الحجم سيكون أكبر من مجموع أحجام الأعضاء والأصناف الأساسية، أما إن كانت المحاذاة مؤلّفة من ‎n‎ بايت، فسيكون الحجم عادةً أصغر مضاعَف لـ‏ ‎n‎، وهو أكبر من حجم جميع الأعضاء والأصناف الأساسية. كذلك سيوضع كل عضو ‎memN‎ في عنوان من مضاعفات ‎alignof(memN)‎، وسيساوي ‎n‎ عادةً محاذاة العضو الذي له أكبر محاذاة.

نتيجة لهذا، في حال أُتبِع عضوٌ بعضو آخر ذي محاذاة أكبر فهناك احتمال بأنّ العضو الأخير لن يُحاذى بشكل صحيح في حال وضعه مباشرة بعد العضو السابق، وهنا سيُوضع الحشو (المعروف أيضًا بمحاذاة العضو - alignment member) بين العضوين، بحيث يتيح للعضو الأخير أن يحصل على المحاذاة المرغوبة. بالمقابل، إذا أُتبِع عضو بعضوٍ آخر ذي محاذاة أصغر، فلن تكون هناك حاجة إلى الحشو، تُعرف هذه العملية أيضًا باسم "التعبئة" (packing).

ونظرًا لأنّ الأصناف تشارك عادة محاذاة أعضائها مع أكبر محاذاة (largest alignof)، فستأخذ الأصناف عادةً محاذاة النوع المُضمن (built-in type) بشكل مباشر أو غير مباشر والذي له أكبر محاذاة. انظر المثال التالي:

افترض أن sizeof(short) == 2 و sizeof(int) == 4 و sizeof(long long) == 8، وكذلك افترض تحديد محاذاة 4-بت للمصرِّف

struct Char { char c; };
// sizeof(Char)            == 1 (sizeof(char))

struct Int { int i; }; 
// sizeof(Int)                == 4 (sizeof(int))

struct CharInt { char c; int i; }; 
// sizeof(CharInt)            == 8 (1 (char) + 3 (padding) + 4 (int))

struct ShortIntCharInt { short s; int i; char c; int j; }; 
// sizeof(ShortIntCharInt)        == 16 (2 (short) + 2 (padding) + 4 (int) + 1 (char) +
//                        3 (padding) + 4 (int))

struct ShortIntCharCharInt { short s; int i; char c; char d; int j; }; 
// sizeof(ShortIntCharCharInt)    == 16 (2 (short) + 2 (padding) + 4 (int) + 1 (char) +
//                        1 (char) + 2 (padding) + 4 (int))

struct ShortCharShortInt { short s; char c; short t; int i; }; 
// sizeof(ShortCharShortInt)    == 12 (2 (short) + 1 (char) + 1 (padding) + 2 (short) +
//                        2 (padding) + 4 (int))

struct IntLLInt { int i; long long l; int j; }; 
// sizeof(IntLLInt)            == 16 (4 (int) + 8 (long long) + 4 (int))
// إذا لم تُحدَّد التعبئة بصراحة، فإنّ معظم المصرفات ستعبّئ هذا مع محاذاة من 8 بتات، بحيث
// sizeof(IntLLInt)            == 24 (4 (int) + 4 (padding) + 8 (long long) +
//                        4 (int) + 4 (padding))

لتكن sizeof(bool) == 1 و sizeof(ShortIntCharInt) == 16، و sizeof(IntLLInt) == 24، والمحاذاة الافتراضية: alignof(ShortIntCharInt) == 4 و alignof(IntLLInt) == 8، نتابع المثال …

struct ShortChar3ArrShortInt {
short s;
 char c3[3];
 short t;
 int i;
};
// ShortChar3ArrShortInt لديه محاذاة 4 بت:    alignof(int) >= alignof(char) &&
//                        alignof(int) >= alignof(short)
// sizeof(ShortChar3ArrShortInt)    ==12 (2 (short) + 3 (char[3]) + 1 (padding) +
//                        2 (short) + 4 (int))
// موضوع عند المحاذاة 2 وليس 4 t لاحظ أنّ
//                        alignof(short) == 2

struct Large_1 {
    ShortIntCharInt sici;
    bool b;
    ShortIntCharInt tjdj;
};

// تساوي 4 بتات Large_1 محاذاة
 // alignof(ShortIntCharInt) == alignof(int) == 4
 // alignof(b) == 1
 // alignof(Large_1) == 4 وعليه تكون
// sizeof(Large_1)        ==36 (16 (ShortIntCharInt) + 1 (bool) + 3 (padding) +
//                    16 (ShortIntCharInt))

struct Large_2
{
    IntLLInt illi;
    float f;
    IntLLInt jmmj;
};
// تساوي 8 بتات Large_2 محاذاة
 // alignof(IntLLInt) == alignof(long long) == 8
 // alignof(float) == 4
 // alignof(Large_2) == 8 وعليه
// sizeof(Large_2) == 56 (24 (IntLLInt) + 4 (float) + 4 (padding) + 24 (IntLLInt))

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

  • في حال فَرْض المحاذاة الصارمة عبر ‎alignas‎، فسيُستخدَم الحشو لإجبار النوع على الالتزام بالمحاذاة المُحدّدة، حتى لو كان أصغر. في المثال أدناه، سيكون لـ ‎Chars<5>‎ ثلاث (أو ربّما أكثر) أُثمونات محشُوّة (padding bytes) مُدرَجة في النهاية لكي يبلغ الحجم الإجمالي 8 بتّات. لا يمكن لصنف محاذاته 4 أن يكون حجمه 5 لأنّه سيكون من المستحيل إنشاء مصفوفة من هذا الصنف، لذلك يجب "تقريب" الحجم إلى أحد مضاعفات العدد 4 عبر حشو البايتات.
// ينبغي أن تكون محاذاة هذا النوع من مضاعفات 4، وينبغي إضافة الحشو عند الحاجة
// Chars<1>..Chars<4> are 4 bytes, Chars<5>..Chars<8> are 8 bytes, etc.
template < size_t SZ>
    struct alignas(4) Chars
    {
        char arr[SZ];
    };
static_assert(sizeof(Chars < 1>) == sizeof(Chars < 4>), "Alignment is strict.\n");
  • إذا كان لعضوين غير ساكنين من صنف معيّن نفس مُحدّد الوصول (access specifier)، فإنّ العضو المُصَرَّح عنه أخيرًا سيأتي آخرًا في تمثيل الكائن. ولكن إذا اختلفت مُحدّدات الوصول، فإنّ ترتيبَهما النسبي داخل الكائن سيكون غير مُحدّد.

  • الترتيب الذي تظهر به الكائنات الفرعية للصنف الأساسي داخل الكائن غير محدّد، سواء أكانت ستظهر بالتتابع، أو ستظهر قبل أو بعد أو بين الكائنات العضوية الفرعية.

الأنواع الحسابية Arithmetic types

أنواع الأحرف الضيقة Narrow character types

يستخدِم نوع الحروف غير المؤشّرة ‎unsigned char‎ كل البتات لتمثيل عدد ثنائي (binary number). لذا إذا كان طول ‎unsigned char‎ يساوي 8 بتات، فإنّ كل الأنماط الممكن تمثيلها بـ 8 بتّات -والتي يبلغ عددها 256- للكائن ‎char‎ ستمثِّل 256 عددًا في المجال {0، 1، …، 255}. العدد 42 مثلا، سيُمثَّل بالسلسلة البتّية ‎00101010‎.

ليس هناك حشو للبتّات في نوع الأحرُف المؤشّرة ‎signed char‎، أي أنّه إذا كان طول ‎signed char‎ يساوي 8 بتات، فسَيستخدم 8 بتات لتمثيل الأعداد. لاحظ أنّ هذه الضمانات لا تنطبق على الأنواع الأخرى.

أنواع الأعداد الصحيحة

تستخدم أنواع الأعداد الصحيحة غير المُؤشّرة نظامًا ثنائيًا خالصًا، لكنّها قد تحتوي على بتّات محشُوّة. على سبيل المثال، من الممكن -رغم بعداحتماله- أن يساوي طول عدد صحيح غير مؤشّر ‎unsigned int‎ ‏64 بتّة، لكن لن يكون بمقدوره تخزين الأعداد الصحيحة بين 0 و ‎‎232 - 1‎‎‎‎ (ضمنيّة)، لأنّ البتات الأخرى البالغ عددها 32 بتة ستكون عبارة عن بتّات حشو، وتلك لا ينبغي كتابتها مباشرة.

تستخدم أنواع الأعداد الصحيحة المُؤشّرة نظامًا ثنائيًا يحتوي على بتّة مخصّصة للإشارة (بتّة الإشارة)، وربّما بتات محشوة. ويكون للقيم التي تنتمي إلى المجال المشترك بين نوع عددي صحيح مؤشّر ونوع عددي صحيح غير مؤشّر، يكون لها نفس التمثيل. على سبيل المثال، إذا كانت السلسلة البتّية ‎0001010010101011‎ لعدد صحيح قصير غير مؤشّر (‎unsigned short‎) تمثّل القيمة ‎5291‎، فإنّها ستمثّل أيضًا القيمة ‎5291‎ عند تفسيرها كعدد قصير (short).

ويحدد التنفيذ أنظمة التمثيل المستخدمة، سواء كانت المكمّل الثنائي (two's complement)، أو المكمّل الأحادي (one's complement)، أو تمثيل الإشارة-السعة (sign-magnitude)، لأنّ الأنظمة الثلاثة تفي بالمتطلّبات الواردة في الفقرة السابقة.

أنواع الأعداد العشرية Floating point types

يتعلق تمثيل أنواع الأعداد العشرية بالتنفيذ، ويتوافق النوعان الأكثر شيوعًا ‎float‎ و ‎double‎ مع توصيف IEEE 754، ويبلغ طولهما 32 و 64 بت (مثلًا، ستتألّف دقّة النوع ‎float‎ من ‏23 بت، مع 8 بتّات للأسّ، وبتّ واحدة للإشارة)، لكن المعيار لا يضمن أيّ شيء، ويعتري تمثيل الأعداد العشرية غالبًا بعض الثغرات، والتي تتسبّب بأخطاء عند استخدامها في العمليات الحسابية.

المصفوفات

ليس هناك حشو بين عناصر أنواع المصفوفات، لذا فالمصفوفة التي تحتوي عناصر من النوع ‎T‎ هي مجرد سلسلة من كائنات ‎T‎ متجاورة في الذاكرة بالترتيب، والمصفوفات متعدّدة الأبعاد هي مصفوفات مكوّنة من مصفوفات، وينطبق عليها ما سبق تكراريًا.

على سبيل المثال، إذا كان لدينا:

int a[5][3];

فتكون ‎a‎ مصفوفة من 5 مصفوفات ثلاثية تحتوي أعدادًا صحيحة (‎int‎)، لذا فإنّ ‎‎‎‎a[0]‎ التي تتكون من العناصر الثلاثة a[0][0]‎ و a[0][1]‎ و a[0][2]‎، توضع في الذاكرة قبل ‎‎‎‎a[1]‎، التي تتكوّن من a[1][0]‎ و a[1][1]‎ و a[1][2]‎. ويُسمّى هذا النظام بالترتيب الكبير للصفوف (row major order).

استنباط النوع Type Inference

يناقش هذا الموضوع استنباط النوع ويشمل الكلمة المفتاحية ‎auto‎ المتاحة منذ الإصدار C++‎ 11.

نوع البيانات: Auto

يوضّح هذا المثال استنباطات النوع الأساسية التي يمكن للمٌصرّف القيام بها.

auto a = 1; // a = int
auto b = 2u; // b = unsigned int
auto c = &a; // c = int*
const auto d = c; // d = const int*
const auto& e = b; // e = const unsigned int&
auto x = a + b // x = int, #compiler warning unsigned and signed
auto v = std::vector<int>; // v = std::vector<int>

لا تنجح الكلمة المفتاحية auto دائمًا في استنباط النوع المتوقع إذا لم تُعطَ تلميحات إضافية بخصوص ‎&‎ أو ‎const‎ أو ‎constexpr‎. في المثال التالي حيث y تساوي unsigned int، لاحظ أننا لا نستطيع استنباط أن y من نوع &const unsigned int، وكان المترجم سينتج نسخة بدلًا من قيمة مرجعية إلى e أو b:

auto y = e;

Lambda auto

يمكن استخدام الكلمة المفتاحية auto للتصريح عن دوالّ لامدا، إذ تساعد على اختصار الشيفرة اللازمة للتصريح عن مؤشّر دالّة.

auto DoThis =[](int a, int b)
{
    return a + b;
};

هذا إن كان Do this من نوع (int)(*DoThis)(int, int)، وإلا فنكتب ما يلي:

int(*pDoThis)(int, int) =[](int a, int b)
{
    return a + b;
};
auto c = Dothis(1, 2);    //      c = int
auto d = pDothis(1, 2);    //      d = int
//      يختصر تعريف دوال لامدا 'auto' استخدام

السلوك الافتراضي إذا لم يُعرَّف نوع القيمة المُعادة لدوال لامدا، هو استنباطها تلقائيًا من عبارة return. في المثال التالي، الأسطر الثلاث التالية متكافئة:

[](int a, int b) -> int { return a + b; };
[](int a, int b) -> auto { return a + b; };
[](int a, int b) { return a + b; };

الحلقات و auto

يوضّح هذا المثال كيف يمكن استخدام auto لاختصار تصريح أنواع حلقات for:

std::map<int, std::string > Map;
for (auto pair: Map)    //      pair = std::pair<int, std::string>
    for (const auto pair: Map)    //      pair = const std::pair<int, std::string >       
        for (const auto &pair: Map)    //      pair = const std::pair<int, std::string>&
            for (auto i = 0; i < 1000; ++i)    //      i = int
                for (auto i = 0; i < Map.size(); ++i)    //    size_t وليس i = int لاحظ أنّ   
                    for (auto i = Map.size(); i > 0; --i)    //      i = size_t

استنتاج الأنواع type deduction

استنتاج مُعامل القالب الخاص بالمنشئات

لم يكن بمقدور "استنتاج القالب" (template deduction) قبل الإصدار C++‎ 17 أن يستنتج نوع الصنف في مُنشئ، بل كان يجب تحديده بشكل صريح، وبما أن تسمية تلك الأنواع مرهقة أحيانًا أو (في حال تعبيرات لامدا) مستحيلة، فقد وُجِدت عدّة مصانع للأنواع (مثل ‎make_pair()‎ و ‎make_tuple()‎ و ‎back_inserter()‎ وما إلى ذلك).

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

غير أن هذا لم يُعدّ هذا ضروريًا بعد الآن:

std::pair p(2, 4.5);    // std::pair<int, double>
std::tuple t(4, 3, 2.5);    // std::tuple<int, int, double>
std::copy_n(vi1.begin(), 3,
    std::back_insert_iterator(vi2));    // back_insert_iterator<std::vector < int>> إنشاء
std::lock_guard lk(mtx);    // std::lock_guard < decltype(mtx)>

يُتوقّع من المُنشئات استنتاج معامِلات قالب الصنف (class template parameters)، لكن هذا قد لا يكفي أحيانًا، لذا يمكننا تقديم بعض التوجيهات الصريحة لتسهيل الاستنتاج:

template <class Iter>
vector(Iter, Iter) -> vector<typename iterator_traits<Iter>::value_type>
int array[] = {1, 2, 3};
std::vector v(std::begin(array), std::end(array));   // std::vector < int> استنتاج

استنتاج النوع عبر Auto

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

يعمل استنتاج النوع باستخدام الكلمة المفتاحية ‎auto‎ بطريقة مشابهة لاستنتاج نوع القالب (Template Type Deduction). انظر الأمثلة التالية:

x في الشيفرة أدناه ليست لا مؤشرًا ولا مرجعًا، بل هي من النوع int، وcx ليست هذا ولا ذاك أيضًا، وإنما هي من نوع const int، بينما تكون rx مرجعًا غير عام (non-universal)، فهي مرجع إلى ثابت. انظر:

auto x = 27;
const auto cx = x;    
const auto& rx = x;  

وفي الشيفرة أدناه، تكون x عددًا صحيحًا int وقيمة يسارية أيضًا lvalue، وعليه يكون نوع uref1 هو &int، وبالمثل فإن cx نوعها const int وقيمة يسارية، لذا يكون uref2 من نوع & const int. أما 27 فهي عدد صحيح وقيمة يسارية، لذا يكون uref3 من نوع &&int. انظر ..

auto&& uref1 = x;
auto&& uref2 = cx;    
auto&& uref3 = 27;    

الاختلافات بين المثالين السابقين مُوضّحة أدناه: يكون نوع x1 و x2 هو int وقيمتهما 27، أما x3 و x4 فنوعهما <std::initializer_list<int وقيمة كل منهما { 27 }. قد يُستنتج النوع في بعض المصرِّفات على أنه int مع قيمة تساوي 27.

auto x1 = 27;  
auto x2(27);    
auto x3 = { 27 };
auto x4{ 27 }; 

auto x5 = { 1, 2.0 }    // error! can't deduce T for std::initializer_list < t>

إذا استخدمت مُهيّئات الأقواس المعقوصة (braced initializers)، فسيُفرض على auto إنشاء متغيّر من النوع std::initializer_list<T>‎، وإذا لم يكن من الممكن استنتاج ‎T‎، فستُرفض الشيفرة.

عندما تُستخدَم ‎auto‎ كنوع القيمة المُعادة من دالّة، فإنّ نوع الإعادة سيكون زائدًا (trailing return type).

auto f() -> int {
 return 42;
}

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

يسمح الإصدار C++‎ 14، بالإضافة إلى استخدام auto المسموح بها في C++‎ 11، بما يلي:

  1. عند استخدامها كنوع للقيمة المُعادة من دالة بدون نوع إعادة زائد (trailing return type)، فإنّها تشير إلى أنّ النوع المُعاد من الدالة يجب أن يُستنتَج من تعليمات الإعادة في متن الدالّة، إن وُجِدت.
// int تعيد f 
auto f() { return 42; } 

// void تعيد g 
auto g() { std::cout << "hello, world!\n"; }
  1. عند استخدامها مع نوع مُعامل خاص بتعبير لامدا، فإنّها تشير إلى أنّ لامدا عامّة (generic). في المثال أدناه تكون x من نوع const int وقيمتها 126.
auto triple = [](auto x) { return 3*x; };
const auto x = triple(42); 

يستنتج الشكل الخاصّ ‎decltype(auto)‎ النوع باستخدام قواعد استنتاج النوع في ‎decltype‎، وليس قواعد ‎auto‎. في المثال التالي، x عدد صحيح، و y مرجع إلى p*:

int* p = new int(42);
auto x = *p; 
decltype(auto) y = *p;  

في C++‎ 03 والإصدارات الأقدم، كان للكلمة المفتاحية ‎auto‎ معنى مختلف تمامًا، إذ كانت مُحدِّد صنف تخزين (storage class specifier)، وقد ورِثتها من C.

استنتاج نوع القالب

الصيغة العامة للقالب:

template < typename T>
    void f(ParamType param);
f(expr);

الحالة 1: إذا كان ‎ParamType‎ مرجعًا أو مؤشّرًا، ولم يكن مرجعًا عامًا (Universal) أو لاحقًا (Forward). فسيعمل استنتاج النوع بالطريقة التالية: سيتجاهل المُصرّف جزء المرجع إذا كان موجودًا في ‎expr‎، وسيحاول المُصرّف بعد ذلك مطابقة ‎expr‎ الخاص بالنوع مع ‎ParamType‎ لتحديد ‎T‎. في المثال التالي يكون param مرجعًا، وx من نوع int، وcx من نوع const int، أما rx فهو مرجع إلى x كـ const int، انظر:

template < typename T>
    void f(T& param);
int x = 27; 
const int cx = x;    
const int& rx = x;   

وفي بقية المثال أدناه، في حالة (f(x تكون T عددًا صحيحًا، ونوع param هو &int، أما في السطرين الثاني والثالث، تكون T من نوع const int و param من نوع &const int، انظر …

f(x);   
f(cx);    
f(rx);    

الحالة 2: إذا كان ‎ParamType‎ مرجع عامًا أو مرجعًا لاحقًا، فسيكون استنتاج النوع مماثلًا لاستنتاج النوع في الحالة 1 إن كانت ‎expr‎ عبارة عن قيمة يمينية. أمّا إذا كانت ‎expr‎ قيمة يسارية، فسيُستنتَج أنّ ‎T‎ و ‎ParamType‎ مرجعان يساريان. انظر المثال التالي حيث يكون param مرجعًا عامًا، وx من نوع int، وcx من نوع const int، و rx مرجع إلى x كـ const int:

template < typename T>
    void f(T&& param);
int x = 27; 
const int cx = x;   
const int& rx = x; 

في بقية المثال أدناه،

  • (f(x: تكون x قيمة يسارية وعليه فإن T و param يكون نوعهما &int.
  • (f(cx: تكون cx قيمة يسارية وعليه فإن T و param يكون نوعهما &const int.
  • (f(rx: تكون rx قيمة يسارية وعليه فإن T و param يكون نوعهما &const int.
  • (f(27: تكون 27 قيمة يمينية وعليه فإن T تكون int ومن ثم فإن param يكون نوعها &&int.
f(x);    
f(cx);   
f(rx);    
f(27);   

الحالة 3: في حال لم يكن ‎ParamType‎ لا مؤشّرًا ولا مرجعًا، فإذا كانت ‎expr‎ مرجعًا أو ثابتةً، فسيُتجاهَل جزء المرجع، أمّا إذا كانت متغيّرة (volatile)، فسيُتجاهَل هذا أيضًا عند استنتاج نوع T. في المثال التالي: تُمرَّر param بالقيمة، وتكون x عددًا صحيحًا int، وcx تكون const int، بينما تكون rx مرجعًا إلى x كـ const int:

template < typename T>
    void f(T param);  
int x = 27;    
const int cx = x;
const int& rx = x;

في بقية المثال أدناه، يكون كل من T و param نوعهما int.

f(x);   
f(cx);    
f(rx);    

نوع الإعادة الزائد Trailing return type

تجنّب تأهيل اسم نوع مُتشعِّب

class ClassWithAReallyLongName
{
    public:
        class Iterator
        { /*... */ };
    Iterator end();
};

تعريف العضو ‎end‎ بنوع إعادة زائد (trailing return type):

auto ClassWithAReallyLongName::end()->Iterator
{
    return Iterator();
}

تعريف العضو ‎end‎ بدون نوع إعادة زائد:

ClassWithAReallyLongName::Iterator ClassWithAReallyLongName::end()
{
    return Iterator();
}

يُبحَثُ عن نوع الإعادة الزائد في نطاق الصنف (scope of the class)، بينما يُبحث عن نوع الإعادة البادئ (leading return type) في نطاق فضاء الاسم (namespace) المحيط، لذا قد يتطّلب ذلك إجراءَ تأهيل "مُكرّر" (redundant qualification).

تعبيرات لامدا Lambda expressions

لا يكون تعبير لامدا إلا نوع إعادة زائد فقط؛ إذ أنّ صيغة نوع الإعادة البادئ لا يمكن تطبيقها على دوالّ لامدا، لاحظ أنّه من غير الضروري غالبًا تحديد النوع المُعاد لأجل دالة لامدا.

struct Base {};
struct Derived1: Base {};
struct Derived2: Base {};
auto lambda =[](bool b)->Base *
{
    if (b) return new Derived1;
    else return new Derived2;
};
// auto lambda = Base*[](bool b) { ... }; صيغة سيئة

Typedef والأسماء البديلة للأنواع

يمكن استخدام الكلمتين المفتاحيتين ‎typedef‎ و (منذ C++‎ 11)‏ ‎using‎ لإعطاء اسم جديد لنوع موجود.

أساسيات صياغة typedef

تصريح ‎typedef‎ يشبه التصريح عن متغيّر أو دالّة، غير أنّها تحتوي كلمة ‎typedef‎، ويؤدّي وجودها إلى التصريح عن نوع بدلاً من التصريح عن متغيّر أو دالّة. انظر المثال التالي: في السطر التالي، T نوعه int:

int T; 

وهنا يكون T اسمًا بديلًا أو كُنية (alias) لـ int:

typedef int T;

نوع A "مصفوفة من 100 عدد صحيح:

int A[100];

أما هنا فيكون A اسمًا بديلًا للنوع "مصفوفة من 100 عدد صحيح":

typedef int A[100];

ونستطيع استخدام الاسم البديل للنوع بالتبادل مع الاسم الأصلي للنوع بمجرد تمام تعريف الأول. انظر المثال التالي حيث تكون S بُنية تحتوي مصفوفة من 100 عدد صحيح:

typedef int A[100];
struct S
{
    A data;
};

لا تنشئ ‎typedef‎ نوعًا مختلفًا، بل تعطينا وسيلة أخرى للإشارة إلى نوع موجود سلفًا.

struct S
{
    int f(int);
};
typedef int I;
I S::f(I x)
{
    return x;
}

استخدامات متقدّمة للكلمة المفتاحية typedef

بما أن تصريحات typedof لها نفس بنية تصريح المتغيرات العادية والدوال، فيمكن استخدام ذلك لقراءة وكتابة تصريحات أعقد. انظر المثال التالي حيث يكون نوع f في السطر الأول مؤشر إلى دالة تأخذ عددا صحيحا وتعيد void، بينما يكون في السطر الثاني اسمًا بديلًا للمؤشر:

void(*f)(int);
typedef void(*f)(int);

هذا مفيد بشكل خاص للبنيات ذات الصياغة المُربِكة، مثل المؤشّرات التي تشير إلى أعضاء غير ساكنة. انظر المثال التالي حيث يكون نوع pmf في السطر الأول مؤشرًا إلى دالة تابعة لـ Foo، يأخذ عددًا صحيحًا ويعيد void، بينما يكون في السطر الثاني اسمًا بديلًا للمؤشر:

void(Foo:: *pmf)(int);
typedef void(Foo:: *pmf)(int);

صيغة تصريحات الدوال التالية صعبة التذكر حتى للمبرمجين ذوي الخبرة:

void(Foo:: *Foo::f(const char *))(int);
int(&g())[100];

ويمكن استخدام ‎typedef‎ لتسهيل قراءة الشيفرة كما في المثال التالي، حيث تكون pmf مؤشرًا إلى نوع دالة تابعة، و f هي دالة تابعة لـ Foo، و ra هي اختصار يعني "مرجع إلى مصفوفة" تتكون في حالتنا هنا من 100 عدد صحيح، وتعيد g مرجعًا إلى مصفوفة من 100 عدد صحيح أيضًا:

typedef void(Foo::pmf)(int);  
pmf Foo::f(const char *);   
typedef int(&ra)[100];  
ra g();  

التصريح عن عدّة أنواع باستخدام typedef

تُعدُّ الكلمة المفتاحية ‎typedef‎ مُحدِّدًا (specifier)، لذا فهي تُطبّق بشكل منفصل على كل مُصرِّح (declarator)، وعليه يشير كل اسم مُصرّح به إلى النوع الذي سيكون لذلك الاسم في غياب ‎typedef‎. انظر المثال التالي حيث يكون نوع x هو *int ونوع p هو ()(*)int، أما في السطر التالي فيكون x اسمًا بديلًا لـ *int، و p اسم بديل لـ ()(*)int:

int *x, (*p)();
typedef int *x, (*p)(); 

التصريح عن الاسم البديل عبر using

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

صيغة ‎using‎ بسيطة للغاية، إذ يوضع الاسم المُراد تعريفه على الجانب الأيسر، بينما يوضع التعريف على الجانب الأيمن.

using I = int;
using A = int[100];    //مصفوفة من 100 عدد صحيح  
using FP = void(*)(int);    // void يعيد  int مؤشر إلى دالة من
using MP = void(Foo:: *)(int);    // void ويعيد int  يأخذ Foo مؤشر إلى دالة تابعة من

إنشاء اسم بديل للنوع باستخدام ‎using‎ له نفس تأثير إنشاء اسم بديل للنوع باستخدام ‎typedef‎، فما هي إلا صيغة بديلة لإنجاز نفس الشيء. وعلى عكس ‎typedef‎، فإنّ ‎using‎ قد تكون مُقولَبة، ويُستخدم مصطلح قالب الاسم البديل "alias template" للإشارة إلى "typedef المُقولبة" (template typedef) والمُنشأة عبر ‎using‎.

نوع الإعادة الزائد Trailing return type

تجنّب تأهيل اسم نوع مُتشعِّب

class ClassWithAReallyLongName
{
    public:
        class Iterator
        { /*... */ };
    Iterator end();
};

تعريف العضو ‎end‎ بنوع إعادة زائد (trailing return type):

auto ClassWithAReallyLongName::end()->Iterator
{
    return Iterator();
}

تعريف العضو ‎end‎ بدون نوع إعادة زائد:

ClassWithAReallyLongName::Iterator ClassWithAReallyLongName::end()
{
    return Iterator();
}

يُبحَثُ عن نوع الإعادة الزائد في نطاق الصنف (scope of the class)، بينما يُبحث عن نوع الإعادة البادئ (leading return type) في نطاق فضاء الاسم (namespace) المحيط، لذا قد يتطّلب ذلك إجراءَ تأهيل "مُكرّر" (redundant qualification).

تعبيرات لامدا Lambda expressions

لا يكون تعبير لامدا إلا نوع إعادة زائد فقط؛ إذ أنّ صيغة نوع الإعادة البادئ لا يمكن تطبيقها على دوالّ لامدا، لاحظ أنّه من غير الضروري غالبًا تحديد النوع المُعاد لأجل دالة لامدا.

struct Base {};
struct Derived1: Base {};
struct Derived2: Base {};
auto lambda =[](bool b)->Base *
{
    if (b) return new Derived1;
    else return new Derived2;
};
// auto lambda = Base*[](bool b) { ... }; صيغة سيئة

محاذاة الأنواع

جميع الأنواع في C++‎ لها محاذاة (alignment) تمثّل قيودًا على عناوين الذاكرة التي يمكن لكائنات تلك الأنواع أن تُنشأ فيها. ويكون عنوان في الذاكرة صالحًا لإنشاء كائن ما إذا كان العنوان قابلًا للقسمة على محاذاة ذلك الكائن. تساوي محاذاة الأنواع دائمًا قوةً للعدد 2 (بما في ذلك العدد 1، والذي يساوي 2 أسّ 0).

التحكم في المحاذاة

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

يمكن استخدام الكلمة المفتاحية ‎alignas‎ لفرض محاذاة معيّنة على متغيّر أو حقل من صنف، أو تصريح صنف أو تعريفه، أو تصريح تعداد أو تعريفه. وتأتي في شكلين:

  • alignas(x)‎ - حيث x تعبير ثابت، يمثّل محاذاة الكيان إن كانت مدعومة.
  • alignas(T)‎ - حيث T يمثّل نوعًا، ويجعل محاذاة الكيان مساوية لمحاذاة النوع T، أي alignof(T)‎ إذا كانت مدعومة.

ستُطبَّق المحاذاة الأكثر صرامة إذا تم تطبيق عددة محدِّدات على نفس الكيان.

في المثال التالي، نضمن أن يكون للمخزن المؤقّت ‎buf‎ المحاذاة المناسبة لتخزين كائن ‎int‎ رغم أنّ نوع عناصره هو ‎unsigned char‎، والذي قد تكون محاذاته أضعف.

alignas(int) unsigned char buf[sizeof(int)];
new(buf) int(42);

لا يمكن استخدام ‎alignas‎ لجعل محاذاة نوع معيّن أصغر من المحاذاة الطبيعية التي كان سيحصل عليها بدونها، انظر المثال التالي حيث تُعد صيغة السطر الأول خاطئة إلا إن كانت محاذاة int هي 1 بايت، وكذلك في السطر الثاني تكون خاطئة إلا إن كانت محاذاة int تساوي محاذاة char أو أقل منها.

alignas(1) int i;  
alignas(char) int j;    

يجب تمرير محاذاة صالحة لـ ‎alignas‎ عند إعطائها تعبيرًا ثابتًا صحيحًا، وينبغي للمحاذاة الصالحة أن تساوي دائمًا قوةً للعدد 2، ويجب أن تكون أكبر من الصفر. وتُلزَم المٌصرّفات بدعم جميع قيم المحاذاة الصالحة شرط ألًا تتجاوز محاذاة النوع ‎std::max_align_t‎، لكن من الممكن أن تدعم محاذاة أكبر من ذلك، إلا أن دعم تخصيص الذاكرة لمثل هذه الكائنات محدود، كما أنّ الحد الأعلى للمحاذاة يتعلّق بالتنفيذ.

توفّر C++‎ 17 دعمًا مباشرًا في ‎operator new‎ لتخصيص الذاكرة للأنواع ذات المحاذاة الزائدة (over-aligned types).

الاستعلام عن محاذاة نوع

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

يمكن الاستعلام عن محاذاة نوع معيّن باستخدام الكلمة المفتاحية ‎alignof‎ كمُعامل أحادي، وتكون لنتيجة تعبيرًا ثابتًا من النوع ‎std::size_t‎، مما يعني إمكانية تقييمه في وقت التصريف.

#include <iostream>
int main()
{
    std::cout << "The alignment requirement of int is: " << alignof(int) << '\n';
}

خرج محتمل:

The alignment requirement of int is: 4

تعيد المحاذاةَ المطلوبة لنوع عناصر في مصفوفة في حال تطبيقها على تلك المصفوفة، أمّا في حال تطبيقها على نوع مرجع (reference type)، فستعيد محاذاة النوع الذي يشير إليه ذلك المرجع، إذ أن المراجع بحد ذاتها ليس لها محاذاة، لأنّها ليست كائنات.

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

ترجمة -بتصرّف- للفصول التالي:

  • Chapter 84: RTTI: Run-Time Type Information
  • Chapter 89: Atomic Types
  • Chapter 90: Type Erasure
  • Chapter 91: Explicit type conversions
  • Chapter 92: Unnamed types
  • Chapter 93: Type Traits
  • Chapter 94: Return Type Covariance
  • Chapter 95: Layout of object types
  • Chapter 96: Type Inference
  • Chapter 97: Typedef and type aliases
  • Chapter 98: type deduction
  • Chapter 99: Trailing return type
  • Chapter 100: Alignment

من كتاب C++ Notes for Professionals





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


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



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

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

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


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

تسجيل الدخول

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


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