سلسلة ++c للمحترفين الدرس 37: السلوك المتعلق بالتنفيذ Implementation-defined behavior في Cpp


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

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

الأنواع التالية هي أنواع عددية صحيحة:

  • ‎char‎
  • الأنواع العددية الصحيحة المُؤشّرة Signed integer types
  • الأنواع العددية الصحيحة غير المُؤشّرة Unsigned integer types
  • ‎char16_t‎ و ‎char32_t‎
  • ‎bool‎
  • ‎wchar_t‎

باستثناء ‎sizeof(char)‎ و ‎sizeof(signed char)‎ و ‎sizeof(unsigned char)‎، الموجودة بين الفقرة ‎‎ 3.9.1.1 [basic.fundamental/1] ‎‎ و‎‎الفقرة 5.3.3.1 ‎‎[expr.sizeof]‎‎ و ‎sizeof(bool)‎، والتي تتوقف على التنفيذ بالكامل وليس لها حدّ أدنى، فإنّّ متطلّبات الحد الأدنى لأحجام هذه الأنواع موجود في القسم 3.9.1 ‏‎[basic.fundamental]‎‎ من المعيار، وسنوضّحه أدناه.

حجم char

تحدّد جميع إصدارات معيار C++‎، في الفقرة 5.3.3.1، أنّ ‎sizeof‎ تعيد القيمة ‎1‎ لكلّ من ‎unsigned char‎ و ‎signed char‎ و ‎char‎ (مسألة ما إذا كان النوع ‎char‎ مؤشّرا - ‎signed‎ - أو غير مؤشّر - ‎unsigned‎ - تتعلّق بالتنفيذ).

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

النوع ‎char‎ كبير بما يكفي لتمثيل 256 قيمة مختلفة، وهو مناسب لتخزين وحدات رموز UTF-8.

حجم الأنواع العددية الصحيحة المُؤشّرة وغير المُؤشّرة

تنصّ المواصفات القياسية، في الفقرة 3.9.1.2، أنّه في قائمة أنواع الأعداد الصحيحة القياسية المُؤشّرة، التي تتكون من ‎signed char‎ و ‎short ‎int‎ و ‎int‎ و ‎long ‎int‎ و ‎long ‎long ‎int‎، فإنّّ كل نوع سيوفّر مساحة تخزينية تكافئ على الأقل المساحة التخزينيّة للنّوع السابق في القائمة. إضافة لذلك، وكما هو مُوضّح في الفقرة 3.9.1.3، كل نوع من هذه الأنواع يقابله نوع صحيح قياسي غير مُؤشّر، هذه الأنواع هي:

  • unsigned char
  • unsigned short int
  • unsigned int
  • unsigned long int
  • unsigned long long int

هذه الأنواع في النهاية لها نفس حجم ومُحاذاة النوع المؤشّر المقابل بالإضافة إلى ذلك، وكما هو مُوضّح في الفقرة 3.9.1.1، النوع ‎char‎ له نفس متطلّبات ‎signed char‎ و ‎unsigned char‎ فيما يخصّ الحجم والمحاذاة.

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

قبل الإصدار C++‎ 11، لم يكن ‎long long‎ و ‎unsigned long long‎ جزءًا من معيار C++‎. لكن بعد إدخالهما في لغة C في معيار C99، دعمت العديدُ من المصرّفات النوع ‎long long‎ كنوع عددي صحيح مؤشّر، و unsigned long long كنوع عددي صحيح غير مؤشّر موسّع، له نفس قواعد أنواع C.

يضمن المعيار ما يلي:

1 == sizeof(char) == sizeof(signed char) == sizeof(unsigned char)
<= sizeof(short) == sizeof(unsigned short)
<= sizeof(int) == sizeof(unsigned int)
<= sizeof(long) == sizeof(unsigned long)

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

<= sizeof(long long) == sizeof(unsigned long long)

لا ينصّ المعيار على حد أدنى للأحجام لكل نوع على حدة. بدلاً من ذلك، لكلّ نوعٍ من الأنواع مجالًا من الحدود الدنيا التي يمكن أن يدعمها، والتي تكون موروثة على النحو المُوضّح في الفقرة 3.9.1.3 من معيار C، في الفقرة 1.2.4.2.1. ويمكن استنتاج الحد الأدنى لحجم نوع ما من ذلك المجال من خلال تحديد الحد الأدنى لعدد البتّات المطلوبة، لاحظ أنّه في معظم الأنظمة قد يكون المجال المدعوم الفعلي لأيّ نوع أكبر من الحد الأدنى، وأنه في الأنواع المُؤشّرة تتوافق المجالات مع مكمّل (complement) واحد، وليس مع مكمّلين اثنين كما هو شائع؛ وذلك لتسهيل توافق مجموعة واسعة من المنصات مع المعايير.

النوع النطاق الأدنى العدد الأدنى المطلوب للبِتَّات
signed char -127 إلى 127 8
unsigned char 0 إلى 255 8
signed short -32,767 إلى 32,767 16
unsigned short 0 إلى 65,535 16
signed int -32,767 إلى 32,767 16
unsigned int 0 إلى 65,535 16
signed long -2,147,483,647 إلى 2,147,483,647 32
unsigned long 0 إلى 4,294,967,295 32

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

النوع النطاق الأدنى العدد الأدنى المطلوب للبِتَّات
signed long long -9,223,372,036,854,775,807 إلى 9,223,372,036,854,775,807 64
unsigned long long 0 إلى 18,446,744,073,709,551,615 64

قد تختلف أحجام الأنواع من تنفيذ لآخر بما أنه يُسمح للأنواع أن تكون أكبر من الحد الأدنى لمتطلّبات الحجم، وأبرز مثال على ذلك تجده في نموذَجي البيانات المخزّنة على 64 بتّة: LP64 و LLP64، إذ أنّه في أنظمة LLP64 (مثل Windows 64-bit)، فإنّ الأنواع ‎ints‎ و ‎long‎ تُخزّن على 32 بتّة (32-bit)، وفي LP64 (مثل 64-bit Linux)، فإنّ ‎int‎ مخزّنة على 32 بتّة، أمّا ‎long‎ فمُخزّنة على 64 بتّة. لهذا من الخطأ افتراض أنّ أنواع الأعداد الصحيحة لها نفس الحجم في جميع الأنظمة.

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

إذا كانت هناك حاجة إلى أنواع عددية صحيحة ذات أحجام ثابتة فاستخدم أنواعًا من الترويسة ، لكن لاحظ أنّ المعيار لا يجبر التنفيذ (implementations) على دعم الأنواع ذات الحجم المضبوط:

  • ‎int8_t‎
  • ‎int16_t‎
  • ‎int32_t‎
  • ‎int64_t‎
  • ‎intptr_t‎
  • ‎uint8_t‎
  • ‎uint16_t‎
  • ‎uint32_t‎
  • ‎uint64_t‎
  • ‎uintptr_t‎

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

حجم char16_t‎ و char32_t‎

يتعلق حجم ‎char16_t‎ و ‎char32_t‎ بالتنفيذ كما ينصّ على ذلك المعيار في الفقرة 5.3.3.1، مع الشروط الواردة في الفقرة 3.9.1.5:

  • ‎char16_t‎ كبير بما يكفي لتمثيل أيّ وحدة رمز من UTF-16، كما أنّ له نفس الحجم والإشارة والمحاذاة التي للنوع ‎uint_least16_t‎، وعليه فحجمه يساوي 16 بتّة على الأقل.
  • ‎char32_t‎ كبير بما يكفي لتمثيل أي وحدة رمز في UTF-32، كما أنّ له نفس الحجم والإشارة والمحاذاة التي للنوع ‎uint_least32_t‎، وعليه فيجب أن يساوي حجمه 32 بتّة على الأقل.

حجم bool

يتعلق حجم ‎bool‎ بالتنفيذ، ولا يساوي بالضرورة ‎1‎.

حجم wchar_t

‎wchar_t‎، كما هو مُوضّح في الفقرة 3.9.1.5 هو نوع متميّز إذ يمكن أن يمثّل مجال قيمه كل وحدات الرموز (code unit) في أكبر مجموعة محارف موسّعة من بين اللغات المدعومة، وله نفس الحجم والإشارة والمحاذاة لأحد الأنواع العددية الصحيحة الأخرى، والذي يمثّل نوعه الأساسي (underlying type).

يتعلق حجم هذا النوع بالتنفيذ كما هو مُعرَّف في الفقرة 5.3.3.1، وقد يساوي على سبيل المثال 8 أو 16 أو 32 بتّة على الأقل؛ وإذا كان النظام يدعم اليونيكود فينبغي أن يُخزّن النوع ‎wchar_t‎ على 32 بتّة على الأقل (باستثناء Windows، إذ يُخزّن النّوع ‎wchar_t‎ على 16 بتّة لأغراض التوافق). وهو موروث من معيار C90، في ISO 9899: 1990 الفقرة 4.1.5، لكن مع بعض التعديلات البسيطة.

ويساوي حجم ‎wchar_t‎ غالبًا 8 أو 16 أو 32 بتّة، وذلك حسب التنفيذ. مثلًا:

  • في أنظمة يونكس والأنظمة المشابهة لها، تُخزّن ‎wchar_t‎ على 32 بتّة، وعادة ما تستخدم في UTF-32.
  • في ويندوز، تُخزّن ‎wchar_t‎ على 16 بتّة، وتُستخدم مع UTF-16.
  • على الأنظمة التي لا تدعم إلّا 8 بتّات فقط، تُخزّن ‎wchar_t‎ على 8 بتّات.

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

إذا كنت تريد دعم اليونيكود، فإنّه يُوصى باستخدام ‎char‎ لأجل الترميز UTF-8، و ‎char16_t‎ لأجل الترميز UTF-16، و ‎char32_t‎ لأجل الترميز UTF-32، بدلاً من استخدام ‎wchar_t‎.

نماذج البيانات

يمكن أن تختلف أحجام أنواع الأعداد الصحيحة بين المنصات كما ذكرنا أعلاه، وما يلي هي أكثر النماذج شيوعًا (الأحجام محسوبة بالبتّات):

النموذج int long مؤشر
LP32 (2/4/4) 16 32 32
ILP32 (4/4/4) 32 32 32
LLP64 (4/4/8) 32 32 64
LP64 (4/8/8) 32 64 64

من بين هذه النماذج:

  • نظام ويندوز 16-بت استخدم LP32.
  • الأنظمة الشبيهة بيونكس مثل يونكس ولينكس ونظام ماك 10 (Mac OSX) وغيرها، ذات معمارية 32 منها، وكذلك نظام ويندوز 32-بت، تستخدم جميعها ILP32.
  • ويندوز ‎‎64-bit يستخدم LLP64.
  • اليونكسات ذات معمارية ‎‎64-bit تستخدم LP64.

لاحظ أنّ هذه النماذج غير مذكورة في المعيار.

النوع Char قد يكون مُؤشَّرًا أو لا

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

مجالات الأنواع العددية

تتعلّق مجالات أنواع الأعداد الصحيحة بالتنفيذ، كما توفّر الترويسة قالب std::numeric_limits<T>‎‎ الذي يوفّر الحدّين الأدنى والأقصى لقيم جميع الأنواع الأساسية.

تفي القيم بالضمانات التي يحدّدها معيار C عبر الترويسات و C++‎ 11 ‎‎‎‎‎‎<=‎‎ ) <cinttypes)

  • std::numeric_limits<signed char>::min()‎‎ - تساوي SCHAR_MIN، وهي أصغر من أو تساوي ‎‎-127.
  • std::numeric_limits::max()‎ تساوي SCHAR_MAX، وهي أكبر من أو تساوي 127.
  • std::numeric_limits<unsigned char>::max()‎‎ تساوي UCHAR_MAX، وهي أكبر من أو تساوي 255.
  • std::numeric_limits<short>::min()‎‎ تساوي SHRT_MIN، وهي أصغر من ‎‎-32767 أو تساويها.
  • std::numeric_limits<short>::max()‎‎ تساوي SHRT_MAX، وهي أكبر من أو تساوي 32767.
  • std::numeric_limits<unsigned short>::max()‎‎ تساويUSHRT_MAX، وهي أكبر من أو تساوي 65535.
  • std::numeric_limits<int>::min() تساوي INT_MIN، وهي أصغر من ‎‎-32767 أو تساويها.
  • std::numeric_limits<int>::max()‎‎ تساوي INT_MAX، وهي أكبر من أو تساوي 32767.
  • ‎‎std::numeric_limits<unsigned int>::max()‎ تساوي UINT_MAX، وهي أكبر من أو تساوي 65535.
  • std::numeric_limits<long>::min()‎‎ تساوي LONG_MIN، وهي أصغر من أو تساوي ‎‎-2147483647.
  • std::numeric_limits<long>::max()‎‎ تساوي LONG_MAX، وهي أكبر من أو تساوي 2147483647.
  • std::numeric_limits<unsigned long>::max()‎‎ تساوي ULONG_MAX، وهي أكبر من أو تساوي 4294967295.

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

  • std::numeric_limits<long long>::min()‎‎ تساوي ‎LLONG_MIN‎، وهي أكبر من أو تساوي ‎‎-9223372036854775807.
  • std::numeric_limits<long long>::max()‎‎ تساوي ‎LLONG_MAX‎، وهي أكبر من أو تساوي 9223372036854775807.
  • std::numeric_limits<unsigned long long>::max() تساوي ‎ULLONG_MAX‎، وهي أكبر من أو تساوي 18446744073709551615.

بالنسبة للنوع العشري‏ ‎T‎، فإنّّ ‎max()‎ تمثّل القيمة المنتهية (finite) القصوى، بينما تمثّل ‎min()‎ الحدّ الأدنى للقيمة المُوحّدة الموجبة.

تمّ توفير بعض الأعضاء الإضافيين للأنواع العشرية، وهي متعلّقة بالتنفيذ أيضًا، ولكنّها تلبّي بعض الضمانات التي يحدّدها المعيار C عبر الترويسة .

  • يعيد ‎digits10‎ عدد الأرقام العشرية الخاصّة بالدقة.
  • std::numeric_limits<float>::digits10 تساوي FLT_DIG، والتي لا تقلّ عن 6.
  • std::numeric_limits<double>::digits10 تساوي DBL_DIG، والتي لا تقلّ عن 10.
  • ‎‎std::numeric_limits<long double>::digits10 تساوي LDBL_DIG، والتي لا تقلّ عن 10.
  • العضو ‎min_exponent10‎ هو الحد الأدنى السلبي E بحيث يكون 10 أسّ E طبيعيًّا.
  • std::numeric_limits<float>::min_exponent10 تساوي ‎FLT_MIN_10_EXP‎، والتي يساوي على الأكثر ‎‎-37.
  • std::numeric_limits<double>::min_exponent10 تساوي ‎DBL_MIN_10_EXP‎، والتي تساوي على الأكثر ‎‎-37.
  • std::numeric_limits<long double>::min_exponent10 تساوي ‎LDBL_MIN_10_EXP‎، والتي تساوي على الأكثر ‎‎-37.
  • العضو ‎max_exponent10‎ هو الحد الأقصى E بحيث يكون 10 أسّ E منتهيًا (finite).
  • std::numeric_limits<float>::max_exponent10 يساوي ‎FLT_MIN_10_EXP‎، ولا يقل عن 37.
  • std::numeric_limits<double>::max_exponent10 يساوي ‎DBL_MIN_10_EXP‎، ولا يقل عن 37.
  • std::numeric_limits<long double>::max_exponent10 تساوي ‎LDBL_MIN_10_EXP‎، ولا يقل عن37.
  • إذا كان العضو ‎is_iec559‎ صحيحًا، فإنّّ النوع سيكون مطابقًا للمواصفات IEC 559 / IEEE 754، وبالتالي سيُحدَّد مجاله من قبل المعيار.

تمثيل قيم الأنواع العشرية

ينصّ المعيار أن لا تقلّ دقة النوع ‎long double‎ عن دقة النوع ‎double‎، والذي ينبغي ألّا تقل دقّته عن دقّة النوع ‎float‎؛ وأنّ النوع ‎long double‎ ينبغي أن يكون قادرًا على تمثيل أيّ قيمة يمثّلها النوع ‎double‎، وأن يمثّل ‎double‎ أيّ قيمة يمكن أن يمثّلها النوع ‎float‎، أما تفاصيل التمثيل فتتعلّق بالتنفيذ.

وبالنسبة لنوع عشري ‎T‎، فإنّّ ‎std::numeric_limits<T>::radix‎ تحدّد الجذر المُستخدم في تمثيل ‎T‎، وإذا كانت ‎std::numeric_limits<T>::is_iec559‎ صحيحة، فإنّّ تمثيل ‎T‎ يطابق أحد التنسيقات المُعرَّفة من قبل معيار IEC 559 / IEEE 754.

التدفق الزائد عند التحويل من عدد صحيح إلى غد صحيح مُؤشّر

عند تحويل عدد صحيح مؤشّر أو غير مؤشّر إلى نوع عددي صحيح مؤشّر ولا تكون قيمته قابلة للتمثيل في النوع المقصود، فإنّّ القيمة المُنتجة تتعلّق بالتنفيذ. مثلًا، لنفرض أن مجال النوع signed char في هذا التنفيذ يكون [-128,127]، ومجال النوع unsigned char من 0 حتى 255:

int x = 12345;
signed char sc = x;    // معرفة بالتنفيذ sc قيمة
unsigned char uc = x;    // مهيأة عند القيمة 57 uc 

النوع الأساسي وحجم التعدادات

إذا لم يكن النوع الأساسي (underlying type) لنوع تعدادي مُحدّدا بشكل صريح، فإنّّه سيُعرَّف من قبل التنفيذ.

enum E
{
    RED,
    GREEN,
    BLUE,
};
using T = std::underlying_type<E>::type;    // يعرِّفه التنفيذ

ومع ذلك، فإنّّ المعيار ينصّ على ألّا يكون نوع التعداد الأساسي أكبر من ‎int‎ إلّا إن لم يكن النوعان ‎int‎ و ‎unsigned‎ ‎int‎ قادريْن على تمثيل جميع قيم التعداد. لذلك في الشيفرة أعلاه، النوع T يمكن أن يكون ‎int‎ أو ‎unsigned int‎ أو ‎short‎ ولكن ليس ‎long long‎ مثلًا. لاحظ أنّ التعداد له نفس حجم نوعه الأساسي (كما يعيده التابع ‎sizeof‎).

القيمة العددية لمؤشر

نتيجة تحويل مؤشّر إلى عدد صحيح باستخدام ‎reinterpret_cast‎ تتعلّق بالتنفيذ، لكن "… يُهدف إلى ألا تكون النتيجة مفاجئة للمطوّرين الذين يعرفون نظام العنونة في الجهاز."

int x = 42;
int *p = &x;
long addr = reinterpret_cast<long> (p);
std::cout << addr << "\n";    // طبع رقم عنوان

وبالمثل، فإنّّ المُؤشر الناتج عن تحويل عدد صحيح يكون أيضًا متعلّقًا بالتنفيذ، والطريقة الصحيحة لتخزين مؤشّر كعدد صحيح هي استخدام النوعين ‎uintptr_t‎ أو ‎intptr_t‎، انظر المثال التالي حيث لم يكن uintptr_t في C++03 وإنما في C99، كنوع اختياري في الترويسة :

#include <stdint.h>

uintptr_t uip;

الإصدار ≥ C++‎ 11 يوجد std::uintptr_t اختياري في C++11:

#include <cstdint>

std::uintptr_t uip;

تُحيل C++‎ 11 إلى C99 لتعريف ‎uintptr_t‎ (المعيار C99، ‏6.3.2.3):

اقتباس

يتميّز النوع العددي الصحيح غير المُؤشّر، بإمكانية تحويل أيّ مؤشّر صالح يشير إلى ‎void‎ إليه، ثم إعادة تحويله مرّة أخرى إلى مؤشّر إلى ‎void‎، وتكون النتيجة مساوية للمؤشّر الأصلي.

بالنسبة لغالبية المنصات الحديثة، يمكنك أن تفترض أنّ مساحة العنونة (address space) مسطحة وأنّ الحسابيات على ‎uintptr_t‎ تكافئ الحسابيات على ‎char *‎، ومن الممكن أن يجري التنفيذ أيّ تعديل عند تحويل ‎void *‎ إلى ‎uintptr_t‎ طالما أنّه يمكن عكس التعديل عند التحويل من ‎uintptr_t‎ إلى ‎void *‎.

مسائل تقنية

  • في الأنظمة المتوافقة مع XSI ‏(X/Open System Interfaces)، فإنّّ النوعين ‎intptr_t‎ و ‎uintptr_t‎ إلزاميان، أمّا في الأنظمة الأخرى فهما اختياريان.
  • الدوال ليست كائنات ضمن معيار C، ولا يضمن معيار C أنّ ‎uintptr_t‎ يستطيع تخزين مؤشّر دالة. كذلك يتطّلب التوافق مع POSIX ‏(2.12.3) أنّ:
اقتباس

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

معيار C99 الفقرة 7.18.1:

اقتباس

عندما تختلف typedef في غياب أو وجود الحرف الأوّل u، فيجب أن تشير إلى الأنواع المُؤشّرة وغير المُؤشّرة المقابلة كما هو مُوضّح في الفقرة 6.2.5؛ وأيّ تنفيذ يوفّر أحد هذين النوعين ينبغي أن يوفّر أيضًا النوع المقابل لآخر.

قد يكون ‎uintptr_t‎ مناسبًا إذا كنت تريد العمل على بتّات المؤشّر بشكل قد يتعذّر في حال استخدمت عددًا صحيحًا مؤشّرًا.

عدد البتات في البايت

البايت (byte) في C++‎ هو المساحة التي يشغلها كائن ‎char‎، وعدد البتات في البايت مُحدّد في ‎CHAR_BIT‎، ومُعرّف في ‎climits‎ ولا يقل عن 8. وفي حين أن عدد بتّات البايت في معظم الأنظمة الحديثة هو 8 وأن أنظمة POSIX تتطلّب أن يساوي ‎CHAR_BIT‎ القيمة 8 بالضبط، إلا أنّ هناك بعض الأنظمة التي يكون فيها ‎CHAR_BIT‎ أكبر من 8، فقد يساوي مثلًا 8 أو 16 أو 32 أو 64 بتّة.

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

ترجمة -بتصرّف- للفصل Chapter 71: Implementation-defined behavior من كتاب C++ Notes for Professionals





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


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



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

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

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


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

تسجيل الدخول

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


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