แก้ไขข้อผิดพลาดของบิลด์เนื่องจากการขึ้นต่อกันระหว่างคลาส


353

ฉันมักจะพบว่าตัวเองอยู่ในสถานการณ์ที่ฉันกำลังหันหน้าไปทางข้อผิดพลาดการรวบรวม / ลิงเกอร์ในหลายโครงการ 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;
    }
    

23
เมื่อทำงานกับ Visual Studio แฟล็ก / showIncludesช่วยให้หลายอย่างในการดีบักปัญหาประเภทนี้
เช็ด

คำตอบ:


288

วิธีคิดเกี่ยวกับสิ่งนี้คือการ "คิดเหมือนคอมไพเลอร์"

ลองนึกภาพคุณกำลังเขียนคอมไพเลอร์ และคุณเห็นรหัสเช่นนี้

// 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;
}

เมื่อคุณกำลังรวบรวมซีซีไฟล์ (จำไว้ว่าซีซีและไม่.hเป็นหน่วยของการรวบรวม) Aคุณจะต้องจัดสรรพื้นที่สำหรับวัตถุ ถ้าอย่างนั้นพื้นที่เท่าไหร่? พอที่จะเก็บB! ตอนนี้ขนาดเท่าBไหร่ พอที่จะเก็บA! อุ่ย

เห็นได้ชัดว่าการอ้างอิงแบบวงกลมที่คุณต้องทำลาย

คุณสามารถทำลายมันได้โดยอนุญาตให้คอมไพเลอร์จองพื้นที่มากเท่าที่รู้เกี่ยวกับ upfront - พอยน์เตอร์และการอ้างอิงตัวอย่างเช่นจะเป็น 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สำหรับขอบเขตและวัตถุประสงค์ทั้งหมด (ถ้าคุณจะออกพรีโพรเซสเซอร์) เพียงสำเนาไฟล์ลงในซีซี ดังนั้นจริงๆ. ccดูเหมือนว่า:

// 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ข้อผิดพลาดร้ายแรง งั้นลองย้ายคำประกาศไปที่Ahเอง

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

และในBhตอนนี้คุณสามารถทำได้#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; 
}

HTH


20
"การบอกผู้แปลเกี่ยวกับ B" เป็นที่รู้จักกันในชื่อการประกาศล่วงหน้าของบี
ปีเตอร์อัจไต

8
พระเจ้าช่วย! พลาดข้อเท็จจริงที่ว่าการอ้างอิงเป็นที่รู้จักในแง่ของพื้นที่ว่าง ในที่สุดตอนนี้ฉันสามารถออกแบบได้อย่างเหมาะสม!
kellogs

47
แต่ก็ยังคุณไม่สามารถใช้ฟังก์ชั่นใด ๆ ใน B (ดังในคำถาม _b-> Printt ())
อันดับที่

3
นี่คือปัญหาที่ฉันมี คุณจะนำฟังก์ชั่นเข้ามาพร้อมกับการประกาศไปข้างหน้าโดยไม่ต้องเขียนไฟล์ส่วนหัวใหม่ทั้งหมดได้อย่างไร?
sydan

2
@sydan: คุณทำไม่ได้ การแก้ไขปัญหาการอ้างอิงแบบวงกลมต้องมีคำจำกัดความออกจากชั้นเรียน
Ben Voigt

101

คุณสามารถหลีกเลี่ยงข้อผิดพลาดในการคอมไพล์ได้หากคุณลบนิยามเมธอดออกจากไฟล์ส่วนหัวและให้คลาสมีเพียงการประกาศเมธอดและการประกาศ / นิยามตัวแปร คำจำกัดความของวิธีการควรอยู่ในไฟล์. cpp (เช่นเดียวกับแนวทางปฏิบัติที่ดีที่สุด)

ด้านล่างของการแก้ปัญหาต่อไปนี้คือ (สมมติว่าคุณได้วางวิธีการในไฟล์ส่วนหัวเพื่อ inline พวกเขา) ว่าวิธีการที่จะไม่ inline โดยคอมไพเลอร์และพยายามใช้คำหลักแบบอินไลน์ผลิตข้อผิดพลาด linker

//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;
}

ขอบคุณ วิธีนี้แก้ปัญหาได้ง่าย ฉันเพียงแค่ย้ายวงกลมรวมถึงไฟล์. cpp
Lenar Hoyt

3
ถ้าคุณมีวิธีการแบบเทมเพลต จากนั้นคุณจะไม่สามารถย้ายมันไปเป็นไฟล์ CPP ได้เว้นแต่คุณจะสร้างอินสแตนซ์ของเทมเพลตด้วยตนเอง
Malcolm

คุณรวม "Ah" และ "Bh" ไว้ด้วยกันเสมอ ทำไมคุณไม่รวม "Ah" ใน "Bh" แล้วรวมเฉพาะ "Bh" ในทั้ง "A.cpp" และ "B.cpp"
Gusev Slava

28

ฉันตอบคำถามนี้ช้า แต่ไม่มีคำตอบที่สมเหตุสมผลถึงแม้จะเป็นคำถามยอดนิยมพร้อมคำตอบที่ได้รับการตอบกลับสูงมาก ....

แนวปฏิบัติที่ดีที่สุด: ส่วนหัวการประกาศไปข้างหน้า

ในฐานะที่เป็นภาพประกอบโดยห้องสมุดมาตรฐานของ<iosfwd>ส่วนหัววิธีการที่เหมาะสมเพื่อให้การประกาศไปข้างหน้าสำหรับคนอื่น ๆ คือการมีส่วนหัวประกาศไปข้างหน้า ตัวอย่างเช่น:

a.fwd.h:

#pragma once
class A;

อา:

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

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

b.fwd.h:

#pragma once
class B;

BH:

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

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

ผู้ดูแลAและBไลบรารีแต่ละคนควรรับผิดชอบในการรักษาส่วนหัวของการประกาศไปข้างหน้าให้ตรงกับส่วนหัวและไฟล์การนำไปใช้งานของตนตัวอย่างเช่น - หากผู้ดูแล "B" เข้ามาและเขียนรหัสใหม่ให้เป็น ...

b.fwd.h:

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

BH:

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เหล่านี้จะไม่มีผลบังคับใช้


2
นี่เป็นวิธีที่สะอาดจริงๆในการจัดการการประกาศไปข้างหน้า "เสียเปรียบ"เท่านั้นที่จะอยู่ในไฟล์พิเศษ ผมถือว่าคุณเสมอรวมa.fwd.hในa.hเพื่อให้มั่นใจว่าพวกเขาอยู่ในซิงค์ โค้ดตัวอย่างหายไปเมื่อใช้คลาสเหล่านี้ a.hและb.hทั้งคู่จะต้องรวมไว้เนื่องจากพวกเขาจะไม่ทำงานแยก: `` `//main.cpp #include" ah "#include" bh "int main () {... }` `หรืออย่างใดอย่างหนึ่ง จะต้องรวมอยู่ในรายการอื่น ๆ เช่นเดียวกับคำถามเปิด ที่b.hรวมa.hและmain.cppรวมถึงb.h
Farway

2
@Farway Right ทุกจำนวน ฉันไม่ได้รำคาญที่จะแสดงmain.cppแต่ยินดีที่คุณได้บันทึกสิ่งที่ควรมีในความคิดเห็นของคุณ ไชโย
Tony Delroy

1
หนึ่งในคำตอบที่ดียิ่งขึ้นพร้อมคำอธิบายโดยละเอียดเกี่ยวกับสาเหตุที่ทำและไม่ควรทำเนื่องจากข้อดีข้อเสีย ...
Francis Cugler

1
@RezaHajianpour: มันสมเหตุสมผลที่จะมีส่วนหัวการประกาศไปข้างหน้าสำหรับทุกชั้นเรียนที่คุณต้องการให้มีการประกาศไปข้างหน้าเป็นวงกลมหรือไม่ ที่กล่าวว่าคุณจะต้องการเมื่อ: 1) รวมถึงการประกาศจริงคือ (หรือสามารถคาดว่าจะกลายเป็นภายหลัง) ราคาแพง (เช่นมีส่วนหัวจำนวนมากหน่วยการแปลของคุณอาจไม่จำเป็น) และ 2) รหัสลูกค้าคือ มีแนวโน้มที่จะสามารถใช้ประโยชน์จากพอยน์เตอร์หรือการอ้างอิงไปยังวัตถุ <iosfwd>เป็นตัวอย่างคลาสสิก: สามารถมีวัตถุสตรีมไม่กี่ที่อ้างอิงจากหลายสถานที่และ<iostream>เป็นจำนวนมากที่จะรวม
Tony Delroy

1
@RezaHajianpour: ฉันคิดว่าคุณมีความคิดที่ถูกต้อง แต่มีปัญหากับคำศัพท์ของคุณ: "เราก็ต้องเป็นประเภทที่จะได้รับการประกาศ " จะได้รับสิทธิ ประเภทที่ประกาศหมายถึงการประกาศล่วงหน้า มันถูกกำหนดเมื่อมีการแยกวิเคราะห์คำจำกัดความแบบสมบูรณ์ (และเพื่อให้คุณอาจต้องการมากกว่านั้น#include)
Tony Delroy

20

สิ่งที่ต้องจำ:

  • สิ่งนี้จะไม่ทำงานหากclass Aมีวัตถุclass Bเป็นสมาชิกหรือในทางกลับกัน
  • ประกาศไปข้างหน้าเป็นวิธีที่จะไป
  • ลำดับของการประกาศมีความสำคัญ (ซึ่งเป็นสาเหตุที่คุณกำลังย้ายคำจำกัดความ)
    • ถ้าทั้งสองคลาสเรียกใช้ฟังก์ชันของอีกตัวคุณต้องย้ายคำจำกัดความออกมา

อ่านคำถามที่พบบ่อย:


1
ลิงก์ที่คุณให้ไว้ไม่ทำงานอีกต่อไปคุณจะรู้จักลิงก์ใหม่ที่อ้างถึงหรือไม่
Ramya Rao

11

ฉันเคยแก้ไขปัญหาแบบนี้โดยการย้ายอินไลน์ทั้งหมดหลังจากนิยามคลาสและวาง#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


ทำไม? ฉันคิดว่ามันเป็นคำตอบที่สง่างามสำหรับปัญหาที่ยุ่งยาก ... เมื่อมีใครต้องการอินไลน์ หากหนึ่งไม่ต้องการ inlines หนึ่งไม่ควรที่จะเขียนรหัสเหมือนมันถูกเขียนขึ้นจากจุดเริ่มต้น ...
epatel

จะเกิดอะไรขึ้นหากผู้ใช้มีB.hก่อน
Mr Fooz

3
โปรดทราบว่าการ์ดส่วนหัวของคุณกำลังใช้ตัวระบุที่สงวนไว้สิ่งใดที่มีเครื่องหมายขีดล่างคู่อยู่สำรองไว้
Lars Viklund

6

ฉันได้เขียนโพสต์เกี่ยวกับสิ่งนี้ครั้งเดียว: การแก้ปัญหาการอ้างอิงแบบวงกลมใน 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;
}

2
โปรดทราบว่าการใช้อินเทอร์เฟซและvirtualมีผลกระทบต่อประสิทธิภาพการทำงานของรันไทม์
cemper93

4

นี่คือโซลูชันสำหรับเทมเพลต: วิธีจัดการกับการขึ้นต่อกันแบบวงกลมด้วยแม่แบบ

เงื่อนงำในการแก้ปัญหานี้คือการประกาศคลาสทั้งสองก่อนที่จะให้คำจำกัดความ (การใช้งาน) ไม่สามารถแยกการประกาศและคำจำกัดความเป็นไฟล์แยกต่างหาก แต่คุณสามารถจัดโครงสร้างได้ราวกับว่าอยู่ในไฟล์แยกกัน


2

ตัวอย่างง่าย ๆ ที่นำเสนอบน Wikipedia ได้ผลสำหรับฉัน (คุณสามารถอ่านคำอธิบายแบบสมบูรณ์ได้ที่http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_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;
}

1

น่าเสียดายที่คำตอบก่อนหน้านี้ทั้งหมดไม่มีรายละเอียดบางอย่าง ทางออกที่ถูกต้องนั้นค่อนข้างยุ่งยาก แต่นี่เป็นวิธีเดียวที่จะทำได้อย่างถูกต้อง และปรับขนาดได้อย่างง่ายดายจัดการการพึ่งพาที่ซับซ้อนมากขึ้นเช่นกัน

นี่คือวิธีที่คุณสามารถทำได้รักษารายละเอียดทั้งหมดและการใช้งาน:

  • วิธีการแก้ปัญหาตรงตามที่ตั้งใจไว้เดิม
  • ฟังก์ชั่นแบบอินไลน์ยังคงอยู่แบบอินไลน์
  • ผู้ใช้AและBสามารถรวม Ah และ Bh ในลำดับใดก็ได้

สร้างสองไฟล์ A_def.h, B_def.h เหล่านี้จะมีเพียงA'และB' s นิยาม:

// 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

จากนั้นอาและ Bh จะมีสิ่งนี้:

// 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ไม่ควรใช้ ส่วนหัวสาธารณะคือ Ah และ Bh


1
สิ่งนี้มีข้อได้เปรียบเหนือโซลูชันของ Tony Delroyหรือไม่ ทั้งสองขึ้นอยู่กับส่วนหัว "ผู้ช่วยเหลือ" แต่โทนี่มีขนาดเล็กกว่า (มีเพียงการประกาศล่วงหน้า) และดูเหมือนว่าพวกเขาจะทำงานในลักษณะเดียวกัน (อย่างน้อยก็ในตอนแรก)
Fabio พูดว่า Reinstate Monica

1
คำตอบนั้นไม่สามารถแก้ปัญหาเดิมได้ มันเพิ่งพูดว่า "นำการประกาศไปข้างหน้าเป็นส่วนหัวแยก" ไม่มีอะไรที่เกี่ยวกับการแก้ไขพึ่งพาวงกลม (คำถามที่ต้องการแก้ปัญหาที่A's และB' s นิยามใช้ได้ไปข้างหน้าประกาศไม่เพียงพอ)
geza

0

ในบางกรณีเป็นไปได้ที่จะกำหนดวิธีการหรือตัวสร้างของคลาส 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();
}

0

น่าเสียดายที่ฉันไม่สามารถออกความเห็นคำตอบจาก geza

เขาไม่ได้เป็นเพียงแค่พูดว่า เขาบอกว่าคุณต้องทำส่วนหัวของคำจำกัดความของคลาสและคำจำกัดความฟังก์ชันอินไลน์เป็นไฟล์ส่วนหัวที่แตกต่างกันเพื่อให้ "การอ้างอิงที่ถูกเลื่อนออกไป"

แต่ภาพประกอบของเขาไม่ค่อยดีนัก เนื่องจากทั้งสองคลาส (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

คำสั่งนี้ควรเลื่อนการประมวลผลของไฟล์ปัจจุบันและดำเนินการรวมที่ค้างอยู่ทั้งหมด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.