首页 文章

由于类之间的循环依赖性而解决构建错误

提问于
浏览
288

我经常发现自己处于这样一种情况:我在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;
}

9 回答

  • 234

    思考这个问题的方法是“像编译器一样思考” .

    想象一下,你正在编写一个编译器 . 你看到这样的代码 .

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

    在编译 .cc 文件时(请记住 .cc 而不是 .h 是编译单元),需要为对象 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 ,对于所有范围和目的(如果取出预处理器)只需将文件复制到 .cc 中 . 所以, .cc 看起来像:

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

    你可以看到为什么编译器无法解决这个问题 - 它不知道 B 是什么 - 它以前从未见过这个符号 .

    那么让我们告诉编译器 B . 这被称为forward declaration,将在this answer中进一步讨论 .

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

    这很有效 . 这不是很好 . 但是在这一点上你应该了解循环引用问题以及我们对它做了什么,尽管修复很糟糕 .

    这个修复不好的原因是因为 #include "A.h" 的下一个人必须在他们可以使用之前声明 B 并且会得到一个可怕的 #include 错误 . 那么让我们将声明转移到 A.h 本身 .

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

    B.h ,此时,您可以直接 #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 .

  • 17

    如果从头文件中删除方法定义并让类仅包含方法声明和变量声明/定义,则可以避免编译错误 . 方法定义应放在.cpp文件中(就像最佳实践指南所说) .

    以下解决方案的缺点是(假设您已将方法放在头文件中以内联它们),编译器不再内联这些方法,并尝试使用inline关键字产生链接器错误 .

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

    要记住的事情:

    • 如果 class A 具有 class B 的对象作为成员,则不起作用,反之亦然 .

    • 前进声明是要走的路 .

    • 申报顺序事项(这就是你要移出定义的原因) .

    • 如果两个类都调用另一个类的函数,则必须移出定义 .

    阅读常见问题:

  • 2

    我曾经通过在类定义之后移动所有内联并且在头文件中的内联之前将 #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 做同样的事情

  • 93

    尽管这是一个受到高度赞扬的答案的热门问题,但我还是没有一个合理的答案 .

    最佳实践:转发声明 Headers

    如标准库的 <iosfwd> Headers 所示,为其他人提供前向声明的正确方法是使用 forward declaration header . 例如:

    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;
    

    b.h:

    #pragma once
    #include "b.fwd.h"
    #include "a.fwd.h"
    
    class B
    {
      public:
        void f(A*);
    };
    

    AB 库的维护者应该负责保持它们的前向声明头与它们的头和实现文件同步,所以 - 例如 - 如果"B"的维护者出现并重写代码...

    b.fwd.h:

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

    b.h:

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

    ...然后重新编译"A"的代码将由包含的 b.fwd.h 的更改触发,并且应该干净地完成 .


    可怜但常见的做法:在其他库中转发声明内容

    说 - 而不是使用前锋声明 Headers 如上所述 - 代码在 a.ha.cc 而不是forward-declares class B; 本身:

    • 如果 a.ha.cc 确实包含 b.h 以后:

    • A的编译一旦到达 B 的冲突声明/定义就会以错误终止(即上述对B的更改打破了A和任何其他滥用前向声明的客户端,而不是透明地工作) .

    否则

    • (如果A最终没有包含 b.h - 如果A只通过指针和/或引用存储/传递Bs,则可能)
      依赖于 #include 分析的
    • 构建工具和更改的文件时间戳在更改为B后不会重建 A (及其进一步依赖的代码),从而导致链接时或运行时出错 . 如果B作为运行时加载的DLL分发,则"A"中的代码可能无法在运行时找到不同的错位符号,这可能会或可能不会处理得足以触发有序关闭或可接受的减少功能 .

    如果A的代码具有旧 B 的模板特化/ "traits",则它们不会生效 .

  • 11

    我曾写过一篇关于此的帖子:Resolving circular dependencies in 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;
    }
    
  • 3

    以下是模板的解决方案:How to handle circular dependencies with templates

    解决这个问题的线索是在提供定义(实现)之前声明这两个类 . 将声明和定义拆分为单独的文件是不可能的,但您可以将它们组织为单独的文件 .

  • 0

    维基百科上提供的简单示例为我工作 . (你可以在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;
    }
    
  • 11

    不幸的是,之前的所有答案都缺少一些细节 . 正确的解决方案有点麻烦,但这是正确完成它的唯一方法 . 它可以轻松扩展,处理更复杂的依赖项 .

    以下是您如何做到这一点,准确保留所有细节和可用性:

    • 解决方案与最初的预期完全相同

    • 内联函数仍然是内联的
      AB

    • 用户可以按任何顺序包含A.h和B.h.

    创建两个文件,A_def.h,B_def.h . 这些只包含 AB 的定义:

    // 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是"private"标头, AB 的用户不应使用它们 . 公共 Headers 是A.h和B.h.

相关问题