解决由于类之间的循环依赖造成的错误

我经常发现自己处于一个C ++项目中由于一些糟糕的devise决策(由别人:)而导致多个编译/链接器错误),导致不同头文件中的C ++类之间循环依赖的情况(也可能发生在同一个文件中) 。 但幸运的是(?)对于我下次再次发生这个问题时,这种情况并没有经常发生。

所以,为了今后方便回忆起来,我将会发表一个代表性的问题和解决scheme。 更好的解决scheme当然是受欢迎的。


  • Ah

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

  • Bh

     #include "Ah" 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 "Bh" #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: Ah class A { B _b; }; // file: Bh class B { A _a; }; // file main.cc #include "Ah" #include "Bh" int main(...) { A a; } 

在编译.cc文件(记住.cc而不是.h是编译单元)时,需要为对象A分配空间。 那么,那么多less空间呢? 足够存储B ! 那么B的大小是多less? 足够存储A ! 哎呀。

显然你必须打破循环的参考。

你可以通过允许编译器保留足够多的空间来解决这个问题,例如,指针和引用总是32位或64位(取决于体系结构),所以如果你replace(任一)一个指针或引用,事情会很好。 假设我们用A代替:

 // file: Ah 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 "Ah" // <-- Houston, we have a problem 

#include ,对于所有的范围和目的(如果你把预处理器)只是将文件复制到.cc 。 所以真的, .cc看起来像:

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

你可以看到为什么编译器不能处理这个问题 – 它不知道B是什么 – 它以前从来没有见过这个符号。

那么让我们告诉编译器关于B 。 这被称为前向声明 ,在这个答案中进一步讨论。

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

工作 。 这不是很好 。 但在这一点上,你应该了解循环引用问题,以及我们做了什么来“修复”它,虽然修复是不好的。

这个修复不好的原因是因为#include "Ah"的下一个人将不得不申报B才能使用它,并会得到一个可怕的#include错误。 那么让我们把声明转移到Ah本身。

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

而在Bh ,在这一点上,你可以直接#include "Ah"

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

HTH。

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

以下解决scheme的缺点是(假设您已经将头文件中的方法放在内联中),这些方法不再由编译器内联,试图使用inline关键字会产生链接器错误。

 //Ah #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 //Bh #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 "Ah" #include "Bh" #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 "Bh" #include "Ah" #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 "Ah" #include "Bh" 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的对象作为成员,则这不起作用,反之亦然。
  • 前向声明是要走的路。
  • 声明的顺序很重要(这就是为什么你要移除定义)。
    • 如果两个类都调用另一个类的函数,则必须将定义移出。

阅读常见问题解答: 39.11 39.12 39.13

我曾经通过在类定义之后移动所有内来解决这类问题,并将其他类的#include放在头文件中的内之前。 通过这种方式,确保在分析内联之前设置所有的定义+内联。

这样做使得在两个(或多个)头文件中仍然有一堆内联。 但有必要有包括警卫

喜欢这个

 // File: Ah #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 "Bh" 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__ */ 

…在Bh做同样的事情

我迟到了回答这个问题,但迄今为止还没有一个合理的答案,尽pipe这是一个受到高度回应的答案。

最佳实践:向前声明标题

正如标准库的<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*); }; 

AB库的维护者都应该负责保持他们的前向声明头文件与他们的头文件和实现文件保持同步,所以 – 例如,如果维护者“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的更改触发,并应该完整地完成。


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

说 – 而不是像上面解释的那样使用前向声明头 – 代替aha.cc而是前向声明class B; 本身:

  • 如果ah或者a.cc后来包含bh
    • A的编译一旦到达B的冲突声明/定义(即上面的B改变破坏了A和任何其他客户滥用前向声明,而不是透明地工作),就会以错误终止。
  • 否则(如果A最终没有包含bh – 如果A只是通过指针和/或引用存储/传递Bs)
    • 依靠#include分析构build工具和更改文件时间戳不会在更改为B之后重buildA (及其进一步相关的代码),从而在链接时或运行时导致错误。 如果B作为运行时加载的DLL分发,“A”中的代码可能无法在运行时find不同的重叠符号,这可能会或可能不会被处理得足以触发有序closures或可接受的缩减function。

如果A的代码具有旧的B模板特化/“特征”,则它们不会生效。

我曾经写过一篇文章: 在c ++中解决循环依赖问题

基本的技术是使用接口来分离类。 所以在你的情况下:

 //Printer.h class Printer { public: virtual Print() = 0; } //Ah #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; } }; //Bh #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 "Ah" #include "Bh" 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; } 

这里是模板的解决scheme: 如何处理模板的循环依赖

解决这个问题的线索是在提供定义(实现)之前声明这两个类。 将声明和定义分割成单独的文件是不可能的,但是你可以像在不同的文件中一样构造它们。

维基百科上提供的简单例子为我工作。 (你可以阅读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 "ah" #include "bh" int main() { A a; B b; ab = &b; ba = &a; }