سؤال

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

لذا، ولأغراض التذكير السهل في المستقبل، سأقوم بنشر مشكلة تمثيلية وحل معها.الحلول الأفضل هي بالطبع موضع ترحيب.


  • A.h

    class B;
    class A
    {
        int _val;
        B *_b;
    public:
    
        A(int val)
            :_val(val)
        {
        }
    
        void SetB(B *b)
        {
            _b = b;
            _b->Print(); // COMPILER ERROR: C2027: use of undefined type 'B'
        }
    
        void Print()
        {
            cout<<"Type:A val="<<_val<<endl;
        }
    };
    

  • B.h

    #include "A.h"
    class B
    {
        double _val;
        A* _a;
    public:
    
        B(double val)
            :_val(val)
        {
        }
    
        void SetA(A *a)
        {
            _a = a;
            _a->Print();
        }
    
        void Print()
        {
            cout<<"Type:B val="<<_val<<endl;
        }
    };
    

  • main.cpp

    #include "B.h"
    #include <iostream>
    
    int main(int argc, char* argv[])
    {
        A a(10);
        B b(3.14);
        a.Print();
        a.SetB(&b);
        b.Print();
        b.SetA(&a);
        return 0;
    }
    
هل كانت مفيدة؟

المحلول

طريقة التفكير في هذا هي "التفكير كمترجم".

تخيل أنك تكتب مترجمًا.وترى رمز مثل هذا.

// file: A.h
class A {
  B _b;
};

// file: B.h
class B {
  A _a;
};

// file main.cc
#include "A.h"
#include "B.h"
int main(...) {
  A a;
}

عندما تقوم بتجميع .نسخة الملف (تذكر أن ملف .نسخة وليس ال هي وحدة الترجمة)، تحتاج إلى تخصيص مساحة للكائن A.حسنًا، كم المساحة إذن؟يكفي للتخزين B!ما هو حجم B ثم؟يكفي للتخزين A!أُووبس.

من الواضح أن مرجعًا دائريًا يجب عليك كسره.

يمكنك كسرها عن طريق السماح للمترجم بحجز المساحة التي يعرفها عن المقدمة - على سبيل المثال، ستكون المؤشرات والمراجع دائمًا 32 أو 64 بت (اعتمادًا على البنية) وهكذا إذا قمت باستبدال (أي منهما) بـ مؤشر أو مرجع، ستكون الأمور رائعة.لنفترض أننا نستبدل بـ A:

// file: A.h
class A {
  // both these are fine, so are various const versions of the same.
  B& _b_ref;
  B* _b_ptr;
};

الآن الأمور أفضل.قليلا. main() لا يزال يقول:

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem

#include, ، لجميع النطاقات والأغراض (إذا قمت بإخراج المعالج المسبق)، فما عليك سوى نسخ الملف إلى ملف .نسخة.حقا، .نسخة يشبه:

// file: partially_pre_processed_main.cc
class A {
  B& _b_ref;
  B* _b_ptr;
};
#include "B.h"
int main (...) {
  A a;
}

يمكنك أن ترى لماذا لا يستطيع المترجم التعامل مع هذا - ليس لديه أي فكرة عن ذلك B هو - لم يسبق له رؤية الرمز من قبل.

لذلك دعونا نخبر المترجم عنه B.يُعرف هذا باسم أ إعلان إلى الأمام, ، ويتم مناقشتها بمزيد من التفصيل في هذه الإجابة.

// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
  A a;
}

هذا يعمل.ليس عظيم.لكن في هذه المرحلة يجب أن يكون لديك فهم لمشكلة المرجع الدائري وما فعلناه "لإصلاحها"، على الرغم من أن الإصلاح سيئ.

سبب سوء هذا الإصلاح هو أن الشخص التالي هو الذي #include "A.h" سوف تضطر إلى إعلان B قبل أن يتمكنوا من استخدامه، وسوف تحصل على رهيب #include خطأ.لذلك دعونا ننقل الإعلان إلى آه بحد ذاتها.

// file: A.h
class B;
class A {
  B* _b; // or any of the other variants.
};

و في ب.ح, ، عند هذه النقطة، يمكنك فقط #include "A.h" مباشرة.

// file: B.h
#include "A.h"
class B {
  // note that this is cool because the compiler knows by this time
  // how much space A will need.
  A _a; 
}

هث.

نصائح أخرى

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

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

//A.h
#ifndef A_H
#define A_H
class B;
class A
{
    int _val;
    B* _b;
public:

    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

//B.h
#ifndef B_H
#define B_H
class A;
class B
{
    double _val;
    A* _a;
public:

    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

//A.cpp
#include "A.h"
#include "B.h"

#include <iostream>

using namespace std;

A::A(int val)
:_val(val)
{
}

void A::SetB(B *b)
{
    _b = b;
    cout<<"Inside SetB()"<<endl;
    _b->Print();
}

void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

//B.cpp
#include "B.h"
#include "A.h"
#include <iostream>

using namespace std;

B::B(double val)
:_val(val)
{
}

void B::SetA(A *a)
{
    _a = a;
    cout<<"Inside SetA()"<<endl;
    _a->Print();
}

void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

//main.cpp
#include "A.h"
#include "B.h"

int main(int argc, char* argv[])
{
    A a(10);
    B b(3.14);
    a.Print();
    a.SetB(&b);
    b.Print();
    b.SetA(&a);
    return 0;
}

أشياء للذكرى:

  • هذا لن ينجح إذا class A لديه كائن من class B كعضو أو العكس.
  • الإعلان إلى الأمام هو وسيلة للذهاب.
  • ترتيب الإعلان مهم (وهذا هو سبب نقل التعريفات).
    • إذا كان كلا الفئتين يستدعيان وظائف الآخر، فيجب عليك نقل التعريفات للخارج.

اقرأ الأسئلة الشائعة:

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

افضل تمرين:رؤوس الإعلان إلى الأمام

كما هو موضح في المكتبة القياسية <iosfwd> header، فإن الطريقة الصحيحة لتقديم إعلانات أمامية للآخرين هي أن يكون لديك رأس الإعلان إلى الأمام.على سبيل المثال:

أ.فود.ح:

#pragma once
class A;

آه:

#pragma once
#include "a.fwd.h"
#include "b.fwd.h"

class A
{
  public:
    void f(B*);
};

ب.fwd.h:

#pragma once
class B;

ب.ح:

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

القائمون على A و B يجب أن تكون كل مكتبات مسؤولة عن الحفاظ على رؤوس الإعلانات الأمامية الخاصة بها متزامنة مع رؤوسها وملفات التنفيذ الخاصة بها، لذلك - على سبيل المثال - إذا جاء مشرف "B" وأعاد كتابة الكود المراد...

ب.fwd.h:

template <typename T> class Basic_B;
typedef Basic_B<char> B;

ب.ح:

template <typename T>
class Basic_B
{
    ...class definition...
};
typedef Basic_B<char> B;

...ثم سيتم تشغيل إعادة ترجمة التعليمات البرمجية لـ "A" من خلال التغييرات التي تم إجراؤها على المضمن b.fwd.h ويجب أن تكتمل بشكل نظيف.


ممارسة سيئة ولكنها شائعة:قم بإعلان الأشياء في libs الأخرى

قل - بدلاً من استخدام رأس إعلان إعادة التوجيه كما هو موضح أعلاه - قم بإدخال الكود a.h أو a.cc بدلا من ذلك يعلن إلى الأمام class B; بحد ذاتها:

  • لو a.h أو a.cc لم تشمل b.h لاحقاً:
    • سيتم إنهاء تجميع A بخطأ بمجرد وصوله إلى الإعلان/التعريف المتعارض B (أي.أدى التغيير المذكور أعلاه إلى B إلى تعطيل A وأي عملاء آخرين يسيئون استخدام التصريحات الآجلة، بدلاً من العمل بشفافية).
  • خلاف ذلك (إذا لم يتضمن A في النهاية b.h - ممكن إذا قام A فقط بتخزين/تمرير Bs بواسطة المؤشر و/أو المرجع)
    • بناء الأدوات التي تعتمد عليها #include لن يتم إعادة إنشاء التحليل والطوابع الزمنية للملفات التي تم تغييرها A (والرمز المعتمد عليها أيضًا) بعد التغيير إلى B، مما يتسبب في حدوث أخطاء في وقت الارتباط أو وقت التشغيل.إذا تم توزيع B كملف DLL تم تحميله في وقت التشغيل، فقد تفشل التعليمات البرمجية الموجودة في "A" في العثور على الرموز المشوهة بشكل مختلف في وقت التشغيل، والتي قد يتم أو لا يتم التعامل معها بشكل جيد بما يكفي لتشغيل إيقاف التشغيل المنظم أو تقليل الوظائف بشكل مقبول.

إذا كان كود A يحتوي على تخصصات القالب/"السمات" للقديم B, ، فلن تصبح نافذة المفعول.

لقد قمت بحل هذا النوع من المشاكل ذات مرة عن طريق تحريك الكل ضمن الصفوف بعد تعريف الفصل ووضع #include للفئات الأخرى قبل ضمن الصفوف في ملف الرأس.بهذه الطريقة، تأكد من تعيين جميع التعريفات + السطور قبل تحليل السطور.

يؤدي القيام بهذا إلى جعل من الممكن الاحتفاظ بمجموعة من السطور في كل من ملفات الرأس (أو المتعددة).ولكن من الضروري أن يكون لديك تشمل الحراس.

مثله

// File: A.h
#ifndef __A_H__
#define __A_H__
class B;
class A
{
    int _val;
    B *_b;
public:
    A(int val);
    void SetB(B *b);
    void Print();
};

// Including class B for inline usage here 
#include "B.h"

inline A::A(int val) : _val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif /* __A_H__ */

...وتفعل الشيء نفسه في B.h

لقد كتبت مقالة عن هذا مرة واحدة: حل التبعيات الدائرية في C++

التقنية الأساسية هي فصل الفئات باستخدام الواجهات.لذلك في حالتك:

//Printer.h
class Printer {
public:
    virtual Print() = 0;
}

//A.h
#include "Printer.h"
class A: public Printer
{
    int _val;
    Printer *_b;
public:

    A(int val)
        :_val(val)
    {
    }

    void SetB(Printer *b)
    {
        _b = b;
        _b->Print();
    }

    void Print()
    {
        cout<<"Type:A val="<<_val<<endl;
    }
};

//B.h
#include "Printer.h"
class B: public Printer
{
    double _val;
    Printer* _a;
public:

    B(double val)
        :_val(val)
    {
    }

    void SetA(Printer *a)
    {
        _a = a;
        _a->Print();
    }

    void Print()
    {
        cout<<"Type:B val="<<_val<<endl;
    }
};

//main.cpp
#include <iostream>
#include "A.h"
#include "B.h"

int main(int argc, char* argv[])
{
    A a(10);
    B b(3.14);
    a.Print();
    a.SetB(&b);
    b.Print();
    b.SetA(&a);
    return 0;
}

هنا هو الحل للقوالب: كيفية التعامل مع التبعيات الدائرية باستخدام القوالب

الحل لحل هذه المشكلة هو الإعلان عن كلا الفئتين قبل تقديم التعريفات (التطبيقات).ليس من الممكن تقسيم الإعلان والتعريف إلى ملفات منفصلة، ​​ولكن يمكنك تنظيمهما كما لو كانا في ملفات منفصلة.

المثال البسيط المقدم على ويكيبيديا كان مفيدًا بالنسبة لي.(يمكنك قراءة الوصف الكامل على http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependeency_in_C.2B.2B )

الملف '''a.h''':

#ifndef A_H
#define A_H

class B;    //forward declaration

class A {
public:
    B* b;
};
#endif //A_H

الملف '''b.h''':

#ifndef B_H
#define B_H

class A;    //forward declaration

class B {
public:
    A* a;
};
#endif //B_H

الملف '''main.cpp''':

#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}

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

وإليك كيفية القيام بذلك، مع الاحتفاظ بكل التفاصيل وسهولة الاستخدام تمامًا:

  • الحل هو بالضبط نفس الحل المقصود في الأصل
  • الوظائف المضمنة لا تزال مضمنة
  • مستخدمي A و B يمكن أن تتضمن A.h وB.h بأي ترتيب

قم بإنشاء ملفين، A_def.h، B_def.h.هذه سوف تحتوي فقط A'رمل Bتعريف:

// A_def.h
#ifndef A_DEF_H
#define A_DEF_H

class B;
class A
{
    int _val;
    B *_b;

public:
    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

// B_def.h
#ifndef B_DEF_H
#define B_DEF_H

class A;
class B
{
    double _val;
    A* _a;

public:
    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

وبعد ذلك، سوف يحتوي A.h وB.h على ما يلي:

// A.h
#ifndef A_H
#define A_H

#include "A_def.h"
#include "B_def.h"

inline A::A(int val) :_val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif

// B.h
#ifndef B_H
#define B_H

#include "A_def.h"
#include "B_def.h"

inline B::B(double val) :_val(val)
{
}

inline void B::SetA(A *a)
{
    _a = a;
    _a->Print();
}

inline void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

#endif

لاحظ أن A_def.h وB_def.h هما رأسان "خاصان"، مستخدمان لـ A و B لا ينبغي استخدامها.الرأس العام هو A.h وB.h.

في بعض الحالات من الممكن أن يُعرِّف طريقة أو مُنشئ من الفئة B في الملف الرأسي من الفئة A لحل التبعيات الدائرية التي تتضمن تعريفات.بهذه الطريقة يمكنك تجنب الاضطرار إلى وضع تعريفات .cc الملفات، على سبيل المثال إذا كنت تريد تنفيذ مكتبة رأس فقط.

// file: a.h
#include "b.h"
struct A {
  A(const B& b) : _b(b) { }
  B get() { return _b; }
  B _b;
};

// note that the get method of class B is defined in a.h
A B::get() {
  return A(*this);
}

// file: b.h
class A;
struct B {
  // here the get method is only declared
  A get();
};

// file: main.cc
#include "a.h"
int main(...) {
  B b;
  A a = b.get();
}

لسوء الحظ لا أستطيع التعليق على الجواب من الجيزة.

إنه لا يقول فقط "ضع الإعلانات في عنوان منفصل".يقول أنه يتعين عليك توزيع رؤوس تعريف الفئة وتعريفات الوظائف المضمنة في ملفات رأس مختلفة للسماح بـ "التبعيات المؤجلة".

لكن تصويره ليس جيدًا حقًا.لأن كلا الفئتين (A و B) تحتاجان فقط إلى نوع غير مكتمل لبعضهما البعض (حقول/معلمات المؤشر).

لفهم الأمر بشكل أفضل، تخيل أن الفئة A بها حقل من النوع B وليس B*.بالإضافة إلى ذلك، تريد الفئتان A وB تعريف دالة مضمّنة بمعلمات من النوع الآخر:

هذا الكود البسيط لن يعمل:

// A.h
#pragme once
#include "B.h"

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}

// B.h
#pragme once
class A;

class B{
  A* b;
  inline void Do(A a);
}

#include "A.h"

inline void B::Do(A a){
  //do something with A
}

//main.cpp
#include "A.h"
#include "B.h"

سينتج عنه الكود التالي:

//main.cpp
//#include "A.h"

class A;

class B{
  A* b;
  inline void Do(A a);
}

inline void B::Do(A a){
  //do something with A
}

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}
//#include "B.h"

لا يتم ترجمة هذا الرمز لأن B::Do يحتاج إلى نوع كامل من A والذي سيتم تعريفه لاحقًا.

للتأكد من أنه يقوم بتجميع الكود المصدري يجب أن يبدو كما يلي:

//main.cpp
class A;

class B{
  A* b;
  inline void Do(A a);
}

class A{
  B b;
  inline void Do(B b);
}

inline void B::Do(A a){
  //do something with A
}

inline void A::Do(B b){
  //do something with B
}

هذا ممكن تمامًا مع هذين الملفين الرأسيين لكل فئة تحتاج إلى تحديد الوظائف المضمنة.المشكلة الوحيدة هي أن الفئات الدائرية لا يمكنها تضمين "الرأس العام" فقط.

لحل هذه المشكلة، أود أن أقترح امتدادًا للمعالج المسبق: #pragma process_pending_includes

يجب أن يؤدي هذا التوجيه إلى تأجيل معالجة الملف الحالي وإكمال جميع التضمينات المعلقة.

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