آلية 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، بما يلي:
- عند استخدامها كنوع للقيمة المُعادة من دالة بدون نوع إعادة زائد (trailing return type)، فإنّها تشير إلى أنّ النوع المُعاد من الدالة يجب أن يُستنتَج من تعليمات الإعادة في متن الدالّة، إن وُجِدت.
// int تعيد f auto f() { return 42; } // void تعيد g auto g() { std::cout << "hello, world!\n"; }
-
عند استخدامها مع نوع مُعامل خاص بتعبير لامدا، فإنّها تشير إلى أنّ لامدا عامّة (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
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.