هل يمكنني الحصول على حاويات متعددة الأشكال ذات دلالات قيمة في لغة C++؟

StackOverflow https://stackoverflow.com/questions/41045

  •  09-06-2019
  •  | 
  •  

سؤال

كقاعدة عامة، أفضّل استخدام دلالات القيمة بدلاً من دلالات المؤشر في لغة C++ (أي استخدام vector<Class> بدلاً من vector<Class*>).عادةً ما يتم التعويض عن الخسارة الطفيفة في الأداء من خلال عدم الاضطرار إلى تذكر حذف الكائنات المخصصة ديناميكيًا.

لسوء الحظ، لا تعمل مجموعات القيمة عندما تريد تخزين مجموعة متنوعة من أنواع الكائنات التي تشتق جميعها من قاعدة مشتركة.انظر المثال أدناه.

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

سؤالي هو:هل يمكنني الحصول على كعكتي (دلالات القيمة) وأكلها أيضًا (حاويات متعددة الأشكال)؟أم يجب علي استخدام المؤشرات؟

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

المحلول

نظرًا لأن الكائنات ذات الفئات المختلفة سيكون لها أحجام مختلفة، فسوف ينتهي بك الأمر إلى مواجهة مشكلة التقطيع إذا قمت بتخزينها كقيم.

أحد الحلول المعقولة هو تخزين مؤشرات ذكية آمنة للحاويات.عادةً ما أستخدم Boost::shared_ptr وهو آمن للتخزين في حاوية.لاحظ أن std::auto_ptr ليس كذلك.

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

يستخدم Shared_ptr حساب المراجع لذلك لن يحذف المثيل الأساسي حتى تتم إزالة جميع المراجع.

نصائح أخرى

نعم يمكنك ذلك.

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

أردت فقط أن أشير إلى أن المتجه <Foo> عادة ما يكون أكثر كفاءة من المتجه <Foo*>.في المتجه <Foo>، ستكون جميع Foos متجاورة في الذاكرة.بافتراض وجود TLB بارد وذاكرة تخزين مؤقت، ستضيف القراءة الأولى الصفحة إلى TLB وتسحب جزءًا من المتجه إلى ذاكرة التخزين المؤقت L#؛ستستخدم القراءات اللاحقة ذاكرة التخزين المؤقت الدافئة وTLB المحملة، مع أخطاء ذاكرة التخزين المؤقت العرضية وأخطاء TLB الأقل تكرارًا.

قارن هذا بالمتجه<Foo*>:أثناء قيامك بملء المتجه، يمكنك الحصول على Foo* من مخصص الذاكرة لديك.بافتراض أن المُخصص الخاص بك ليس ذكيًا للغاية، (tcmalloc؟) أو أنك تملأ المتجه ببطء مع مرور الوقت، فمن المحتمل أن يكون موقع كل Foo بعيدًا عن Foos الأخرى:ربما بمئات البايتات فقط، وربما بالميجابايت.

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

لذا، استمر في استخدام Vector<Foo> كلما كان ذلك مناسبًا.:-)

تريد معظم أنواع الحاويات تجريد استراتيجية التخزين المحددة، سواء كانت قائمة مرتبطة أو متجهة أو قائمة على الشجرة أو أي شيء آخر لديك.لهذا السبب، ستواجه مشكلة في امتلاك واستهلاك الكعكة المذكورة أعلاه (أي أن الكعكة كذبة (ملاحظة:كان على شخص ما أن يلقي هذه النكتة)).

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

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

لقد أمضيت بعض الوقت في الماضي في محاولة تصور كيفية استخدام Virtual Proxy / Envelope-Letter / تلك الخدعة اللطيفة ذات المؤشرات المرجعية لإنجاز شيء مثل أساس البرمجة الدلالية ذات القيمة في C ++.

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

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

أنا يعتقد سوف يرمي bad_any_cast إذا حاولت إرسال أي فئة مشتقة إلى قاعدتها.لقد حاولت ذلك:

  // But you sort of can do it with boost::any.

  vector<any> valueVec;

  valueVec.push_back(any(Parent()));
  valueVec.push_back(any(Child()));        // remains a Child, wrapped in an Any.

  Parent p = any_cast<Parent>(valueVec[0]);
  Child c = any_cast<Child>(valueVec[1]);
  p.write();
  c.write();

  // Output:
  //
  // Parent: 1
  // Child: 2, 2

  // Now try casting the child as a parent.
  try {
      Parent p2 = any_cast<Parent>(valueVec[1]);
      p2.write();
  }
  catch (const boost::bad_any_cast &e)
  {
      cout << e.what() << endl;
  }

  // Output:
  // boost::bad_any_cast: failed conversion using boost::any_cast

بعد كل ما قيل، سأذهب أيضًا إلى طريق Shared_ptr أولاً!مجرد التفكير في هذا قد يكون من بعض الاهتمام.

نلقي نظرة على static_cast و reinterpret_cast
في لغة البرمجة C++، الطبعة الثالثة، يصفها بيارن ستروستروب في الصفحة 130.وهناك قسم كامل عن هذا في الفصل السادس.
يمكنك إعادة صياغة فئة الوالدين الخاصة بك إلى فئة الطفل.هذا يتطلب منك أن تعرف متى يكون كل واحد منها.وفي الكتاب د.يتحدث ستروستروب عن تقنيات مختلفة لتجنب هذا الموقف.

لا تفعل هذا.وهذا ينفي تعدد الأشكال الذي تحاول تحقيقه في المقام الأول!

فقط لإضافة شيء واحد للجميع 1800 معلومة قالها مسبقا.

قد ترغب في إلقاء نظرة على "أكثر فعالية C++" بواسطة سكوت مايرز "البند 3:لا تتعامل أبدًا مع المصفوفات بشكل متعدد الأشكال" من أجل فهم هذه المشكلة بشكل أفضل.

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

هذه فكرة يمكن أن تناسب احتياجاتك.

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

  1. استخدم std::اختياري أو Boost::اختياري ونمط الزائر.يجعل هذا الحل من الصعب إضافة أنواع جديدة، ولكن من السهل إضافة وظائف جديدة.
  2. استخدم فئة مجمعة مشابهة لما يقدم شون بارنت في حديثه.هذا الحل يجعل من الصعب إضافة وظائف جديدة، ولكن من السهل إضافة أنواع جديدة.

يحدد الغلاف الواجهة التي تحتاجها لفئاتك ويحمل مؤشرًا لأحد هذه الكائنات.يتم تنفيذ الواجهة بوظائف مجانية.

فيما يلي مثال على تنفيذ هذا النمط:

class Shape
{
public:
    template<typename T>
    Shape(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {}

    friend void draw(const Shape &shape)
    {
        shape.container->drawImpl();
    }
    // add more functions similar to draw() here if you wish
    // remember also to add a wrapper in the Concept and Model below

private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void drawImpl() const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x) : m_data(move(x)) { }
        void drawImpl() const override
        {
            draw(m_data);
        }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

يتم بعد ذلك تنفيذ أشكال مختلفة كبنيات/فئات عادية.لك الحرية في اختيار ما إذا كنت تريد استخدام وظائف الأعضاء أو الوظائف المجانية (ولكن سيتعين عليك تحديث التنفيذ أعلاه لاستخدام وظائف الأعضاء).أفضّل الوظائف المجانية:

struct Circle
{
    const double radius = 4.0;
};

struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

void draw(const Circle &circle)
{
    cout << "Drew circle with radius " << circle.radius << endl;
}

void draw(const Rectangle &rectangle)
{
    cout << "Drew rectangle with width " << rectangle.width << endl;
}

يمكنك الآن إضافة كليهما Circle و Rectangle كائنات لنفس std::vector<Shape>:

int main() {
    std::vector<Shape> shapes;
    shapes.emplace_back(Circle());
    shapes.emplace_back(Rectangle());
    for (const auto &shape : shapes) {
        draw(shape);
    }
    return 0;
}

الجانب السلبي لهذا النمط هو أنه يتطلب كمية كبيرة من النموذج المعياري في الواجهة، حيث يجب تعريف كل وظيفة ثلاث مرات.الاتجاه الصعودي هو أنك تحصل على دلالات النسخ:

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}

هذا ينتج:

Drew rectangle with width 2
Drew rectangle with width 2

إذا كنت تشعر بالقلق إزاء shared_ptr, ، يمكنك استبداله ب unique_ptr.ومع ذلك، لن يكون قابلاً للنسخ وسيتعين عليك إما نقل كافة الكائنات أو تنفيذ النسخ يدويًا.يناقش Sean Parent هذا الأمر بالتفصيل في حديثه ويظهر التنفيذ في الإجابة المذكورة أعلاه.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top