سؤال

لقد استخدمت النقابات في وقت سابق بشكل مريح. اليوم شعرت بالقلق عندما قرأت هذا المشنور وتعلم أن هذا الرمز

union ARGB
{
    uint32_t colour;

    struct componentsTag
    {
        uint8_t b;
        uint8_t g;
        uint8_t r;
        uint8_t a;
    } components;

} pixel;

pixel.colour = 0xff040201;  // ARGB::colour is the active member from now on

// somewhere down the line, without any edit to pixel

if(pixel.components.a)      // accessing the non-active member ARGB::components

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

تحديث:

أردت توضيح بعض الأشياء بعد فوات الأوان.

  • إجابة السؤال ليست هي نفسها لـ C و C ++ ؛ تم وضع علامة على نفسي الأصغر سناً على أنها C و C ++.
  • بعد البحث عن معيار C ++ 11 ، لم أستطع أن أقول بشكل قاطع أنه يدعو إلى الوصول/فحص عضو في الاتحاد غير النشط غير محدد/غير محدد/محدد. كل ما يمكن أن أجده هو الفقرة 9.5/1:

    إذا كان اتحاد الطبق القياسي يحتوي على العديد من الهياكل القياسية التي تشترك في تسلسل أولي مشترك ، وإذا كان كائن من نوع الاتحاد القياسي هذا يحتوي على واحدة من هياكل الطبقات القياسية ، يُسمح بتفتيش التسلسل الأولي المشترك لأي شخص من أعضاء بنية الطبق القياسية. §9.2/19: تشتركان في هياكل الطبقات القياسية في تسلسل أولي شائع إذا كان للأعضاء المقابلان أنواعًا متوافقة مع التخطيط وإما أن لا يكون أي من الأعضاء حقلًا صغيرًا أو كلاهما حقول بت بنفس العرض لتسلسل واحد أو أكثر أولي أفراد.

  • بينما في ج ، ((C99 TC3 - DR 283 فصاعدا) من القانوني القيام بذلك (بفضل Pascal Cuoq لإحضار هذا). ومع ذلك ، محاولة القيام بها لا يزال بإمكانه أن يؤدي إلى سلوك غير محدد, ، إذا كانت القيمة غير صالحة (تسمى "تمثيل الفخ") للنوع الذي تتم قراءته. خلاف ذلك ، فإن قراءة القيمة محددة.
  • استدعى C89/90 هذا تحت السلوك غير المحدد (الملحق J) وكتاب K&R يقول أنه تم تعريفه. اقتباس من K&R:

    هذا هو الغرض من الاتحاد - وهو متغير واحد يمكن أن يحتفظ بشكل شرعي بأي من الأنواع العديدة. [...] طالما أن الاستخدام ثابت: يجب أن يكون النوع الذي تم استرداده هو النوع الذي تم تخزينه مؤخرًا. تقع على عاتق المبرمج مسؤولية تتبع النوع الذي يتم تخزينه حاليًا في الاتحاد ؛ تعتمد النتائج على التنفيذ إذا تم تخزين شيء ما كنوع واحد واستخراجه كنوع آخر.

  • مقتطف من TC ++ PL من Stroustrup (منجم التأكيد)

    يمكن أن يكون استخدام النقابات ضروريًا لتوافق البيانات [... في بعض الأحيان يساء استخدام "تحويل النوع".

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

هل كانت مفيدة؟

المحلول

الغرض من النقابات واضح إلى حد ما ، ولكن لسبب ما يفتقده الناس في كثير من الأحيان.

الغرض من الاتحاد هو لحفظ الذاكرة باستخدام نفس منطقة الذاكرة لتخزين كائنات مختلفة في أوقات مختلفة. هذا هو.

إنه مثل غرفة في فندق. يعيش أشخاص مختلفين فيه لفترات زمنية غير متداخلة. هؤلاء الأشخاص لا يجتمعون أبدًا ، ولا يعرفون أي شيء عن بعضهم البعض. من خلال إدارة مشاركة الغرف بشكل صحيح (أي بالتأكد من عدم تعيين أشخاص مختلفين إلى غرفة واحدة في نفس الوقت) ، يمكن لفندق صغير نسبيًا توفير أماكن إقامة لعدد كبير نسبيًا من الأشخاص ، وهو ما هو الفنادق من أجل.

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

لسبب ما ، أصبح هذا الغرض الأصلي من الاتحاد "تجاوز" بشيء مختلف تمامًا: كتابة أحد أعضاء الاتحاد ثم فحصه من خلال عضو آخر. هذا النوع من إعادة تفسير الذاكرة (ويعرف أيضًا باسم "Type Punning") هو ليس استخدامًا صحيحًا للنقابات. يؤدي عمومًا إلى سلوك غير محدد يوصف بأنه إنتاج سلوك محدد في التنفيذ في C89/90.

تعديل: باستخدام النقابات لأغراض النوع المعاق (أي كتابة عضو ثم قراءة أخرى) أعطيت تعريفًا أكثر تفصيلاً في أحد corrigenda التقنية لمعيار C99 (انظر DR#257 و DR#283). ومع ذلك ، ضع في اعتبارك أن هذا رسميًا لا يحميك من الركض إلى سلوك غير محدد من خلال محاولة قراءة تمثيل فخ.

نصائح أخرى

يمكنك استخدام النقابات لإنشاء هياكل مثل ما يلي ، والذي يحتوي على حقل يخبرنا أي عنصر الاتحاد يستخدم بالفعل:

struct VAROBJECT
{
    enum o_t { Int, Double, String } objectType;

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;

السلوك غير محدد من وجهة نظر اللغة. النظر في أن المنصات المختلفة يمكن أن يكون لها قيود مختلفة في محاذاة الذاكرة والانحراف. سوف يقوم الرمز الموجود في جهاز Endian كبير مقابل جهاز Endian الصغير بتحديث القيم الموجودة في البنية بشكل مختلف. سيتطلب إصلاح السلوك في اللغة من جميع التطبيقات استخدام نفس قيود محاذاة الذاكرة ...) التي تحد من الاستخدام.

إذا كنت تستخدم C ++ (أنت تستخدم علامتين) وتهتم حقًا بقابلية النقل ، فيمكنك فقط استخدام البنية وتوفير مجموعة من الأدوات التي تأخذ uint32_t ويضع الحقول بشكل مناسب من خلال عمليات bitmask. يمكن القيام الشيء نفسه في C مع وظيفة.

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

أكثر شائع استخدام union أتيت بانتظام هو اسم مستعار.

النظر في ما يلي:

union Vector3f
{
  struct{ float x,y,z ; } ;
  float elts[3];
}

ماذا يفعل هذا؟ يسمح بالوصول النظيف والأنيق من Vector3f vec;أعضاء أيضاً اسم:

vec.x=vec.y=vec.z=1.f ;

أو عن طريق وصول عدد صحيح إلى الصفيف

for( int i = 0 ; i < 3 ; i++ )
  vec.elts[i]=1.f;

في بعض الحالات ، يكون الوصول بالاسم هو أوضح شيء يمكنك القيام به. في حالات أخرى ، خاصةً عندما يتم اختيار المحور برمجيًا ، فإن الشيء الأسهل في القيام به هو الوصول إلى المحور بواسطة الفهرس العددي - 0 لـ X و 1 لـ Y و 2 لـ z.

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

union A {
   int i;
   double d;
};

A a[10];    // records in "a" can be either ints or doubles 
a[0].i = 42;
a[1].d = 1.23;

بالطبع ، تحتاج أيضًا إلى نوع من التمييز لتقول ما يحتويه المتغير فعليًا. ولاحظ أن النقابات في C ++ لا تستخدم كثيرًا لأنها يمكن أن تحتوي فقط على أنواع POD - بفعالية تلك التي لا تحتوي على مُنشئين ومدمرين.

في C كانت طريقة رائعة لتنفيذ شيء مثل البديل.

enum possibleTypes{
  eInt,
  eDouble,
  eChar
}


struct Value{

    union Value {
      int iVal_;
      double dval;
      char cVal;
    } value_;
    possibleTypes discriminator_;
} 

switch(val.discriminator_)
{
  case eInt: val.value_.iVal_; break;

في أوقات ذاكرة litlle ، يستخدم هذا الهيكل ذاكرة أقل من البنية التي تحتوي على جميع الأعضاء.

بالمناسبة يوفر C

    typedef struct {
      unsigned int mantissa_low:32;      //mantissa
      unsigned int mantissa_high:20;
      unsigned int exponent:11;         //exponent
      unsigned int sign:1;
    } realVal;

للوصول إلى قيم البت.

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

في C ++ ، تعزيز البديل قم بتنفيذ نسخة آمنة من الاتحاد ، مصممة لمنع السلوك غير المحدد قدر الإمكان.

أدائها مطابق ل enum + union بنية (مكدس مخصصة أيضًا وما إلى ذلك) ولكنه يستخدم قائمة من الأنواع بدلاً من enum :)

قد يكون السلوك غير محدد ، لكن هذا يعني أنه لا يوجد "معيار". تقدم جميع المترجمين اللائقين #pragmas للتحكم في التعبئة والمحاذاة ، ولكن قد يكون لها افتراضات مختلفة. سوف تتغير الافتراضات أيضًا اعتمادًا على إعدادات التحسين المستخدمة.

أيضا ، النقابات ليست مجرد لتوفير الفضاء. يمكن أن تساعد المترجمين الحديثين مع نوع المعاقبة. اذا أنت reinterpret_cast<> كل شيء لا يمكن للمترجم أن يضع افتراضات حول ما تفعله. قد تضطر إلى التخلص من ما يعرفه عن نوعك والبدء من جديد (إجبار الكتابة على الذاكرة ، وهو أمر غير فعال للغاية هذه الأيام مقارنةً بسرعة ساعة وحدة المعالجة المركزية).

من الناحية الفنية ، فإنه غير محدد ، ولكن في الواقع معظم المترجمين (الكل؟) يعاملونه تمامًا مثل استخدام أ reinterpret_cast من نوع إلى آخر ، يتم تعريف النتيجة. لن أفقد النوم على رمزك الحالي.

للحصول على مثال آخر على الاستخدام الفعلي للنقابات ، يقوم Corba Framework بتسلسل الكائنات باستخدام نهج الاتحاد الموسومة. جميع الفئات المعرفة من قبل المستخدم هي أعضاء في اتحاد واحد (ضخم) ، و معرف عدد صحيح يخبر Demarshaller كيفية تفسير الاتحاد.

وقد ذكر آخرون الاختلافات في الهندسة المعمارية (القليل - الكبير الإنديان).

قرأت المشكلة أنه نظرًا لمشاركة ذاكرة المتغيرات ، ثم عن طريق الكتابة إلى واحد ، يتغير الآخرون ، واعتمادًا على نوعها ، يمكن أن تكون القيمة بلا معنى.

على سبيل المثال. الاتحاد {float f ؛ int أنا ؛ } x ؛

ستكون الكتابة إلى XI بلا معنى إذا قرأت بعد ذلك من XF - إلا إذا كان هذا هو ما تقصده من أجل النظر إلى علامة أو مكونات Mantissa من العائمة.

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

على سبيل المثال. الاتحاد {char c [4] ؛ int أنا ؛ } x ؛

إذا كان ، من الناحية الافتراضية ، على بعض الماكينة ، يجب أن يكون char محاذاة ، فإن C [0] و C [1] سيشاركان التخزين مع I ولكن ليس C [2] و C [3].

بلغة C كما تم توثيقها في عام 1974 ، شارك جميع أعضاء الهيكل مساحة اسم مشتركة ، وكان معنى "PTR-> عضو" يعرف كإضافة إزاحة العضو إلى "PTR" والوصول إلى العنوان الناتج باستخدام نوع العضو. جعل هذا التصميم من الممكن استخدام نفس PTR مع أسماء الأعضاء المأخوذة من تعريفات بنية مختلفة ولكن مع نفس الإزاحة ؛ استخدم المبرمجون تلك القدرة لمجموعة متنوعة من الأغراض.

عندما تم تعيين أعضاء الهيكل مساحات أسمائهم الخاصة ، أصبح من المستحيل إعلان اثنين من أعضاء الهيكل بنفس النزوح. جعلت إضافة النقابات إلى اللغة من الممكن تحقيق نفس الدلالات التي كانت متوفرة في الإصدارات السابقة من اللغة (على الرغم من أن عدم القدرة على الحصول على أسماء تم تصديرها إلى سياق مغلق قد لا يزال ضروريًا باستخدام استفادة/استبدال لاستبدال FOO-> العضو في foo-> type1.member). لم يكن الأمر المهم هو أن الأشخاص الذين أضافوا النقابات لديهم أي استخدام مستهدف معين في الاعتبار ، بل يوفرون وسيلة من خلالها المبرمجين الذين اعتمدوا على الدلالات السابقة ، لأي غرض, ، يجب أن تظل قادرة على تحقيق نفس الدلالات حتى لو اضطروا إلى استخدام بناء جملة مختلف للقيام بذلك.

تستطيع استعمال اتحاد AA لسببين رئيسيين:

  1. طريقة مفيدة للوصول إلى نفس البيانات بطرق مختلفة ، كما هو الحال في مثالك
  2. طريقة لتوفير المساحة عندما يكون هناك أعضاء مختلفون من البيانات يمكن أن يكونوا "نشطين" على الإطلاق

1 هو في الحقيقة أكثر من الاختراق على غرار C للاختصار عن كود الكتابة على أساس أنك تعرف كيف تعمل بنية ذاكرة النظام المستهدف. كما قلت بالفعل ، يمكنك عادة الابتعاد عنها إذا لم تستهدف الكثير من المنصات المختلفة. أعتقد أن بعض المترجمين قد يسمحون لك باستخدام توجيهات التعبئة أيضًا (أعلم أنهم يفعلون على الهياكل)؟

مثال جيد على 2. يمكن العثور عليه في البديل اكتب المستخدمة على نطاق واسع في كوم.

كما ذكر آخرون ، يمكن استخدام النقابات جنبًا إلى جنب مع التعدادات والملفوفة في بنيات لتنفيذ النقابات الموسومة. أحد الاستخدامات العملية هو تنفيذ Rust's Result<T, E>, ، الذي يتم تنفيذه في الأصل باستخدام نقي enum (يمكن للصدأ الاحتفاظ ببيانات إضافية في متغيرات التعداد). هنا مثال C ++:

template <typename T, typename E> struct Result {
    public:
    enum class Success : uint8_t { Ok, Err };
    Result(T val) {
        m_success = Success::Ok;
        m_value.ok = val;
    }
    Result(E val) {
        m_success = Success::Err;
        m_value.err = val;
    }
    inline bool operator==(const Result& other) {
        return other.m_success == this->m_success;
    }
    inline bool operator!=(const Result& other) {
        return other.m_success != this->m_success;
    }
    inline T expect(const char* errorMsg) {
        if (m_success == Success::Err) throw errorMsg;
        else return m_value.ok;
    }
    inline bool is_ok() {
        return m_success == Success::Ok;
    }
    inline bool is_err() {
        return m_success == Success::Err;
    }
    inline const T* ok() {
        if (is_ok()) return m_value.ok;
        else return nullptr;
    }
    inline const T* err() {
        if (is_err()) return m_value.err;
        else return nullptr;
    }

    // Other methods from https://doc.rust-lang.org/std/result/enum.Result.html

    private:
    Success m_success;
    union _val_t { T ok; E err; } m_value;
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top