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


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标志有助于调试此类问题。
wip

Answers:


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

在编译.cc文件时(请记住,.cc而不是.h是编译单元),您需要为object分配空间A。那么,那么多少空间呢?足够存储BB当时的大小是多少?足够存储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。这称为前向声明,并将在此答案中进行进一步讨论。

// 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编译器”被称为B的前向声明
彼得Ajtai

8
我的天啊!完全错过了根据占用空间已知引用的事实。最后,现在我可以正确设计了!
kellogs 2011年

47
但是您仍然不能在B上使用任何功能(如问题_b-> Printt())
rank1 2013年

3
这是我遇到的问题。如何在不完全重写头文件的情况下将函数带入正向声明?
sydan 2015年


101

如果从头文件中删除方法定义并使类仅包含方法声明和变量声明/定义,则可以避免编译错误。方法定义应放在.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;
}

谢谢。这样可以轻松解决问题。我只是将通函包含移至.cpp文件。
Lenar Hoyt 2014年

3
如果您有模板方法怎么办?然后,除非您手动实例化模板,否则您无法真正将其移入CPP文件。
马尔科姆

您始终将“ Ah”和“ Bh”一起包括在内。为什么不在“ Bh”中包含“ Ah”,然后在“ A.cpp”和“ B.cpp”中仅包含“ Bh”?
Gusev Slava

28

我来晚了,但是到目前为止,还没有一个合理的答案,尽管这是一个广受好评的答案。

最佳做法:转发声明标头

如标准库的<iosfwd>标头所示,为其他人提供正向声明的正确方法是拥有一个正向声明标头。例如:

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并且应完全完成。


可怜但很普遍的做法:在其他库中向前声明内容

说-而不是如上所述使用前向声明标头-在自身中进行代码声明a.ha.cc前向声明class B;

  • 如果a.h还是a.cc没有包括b.h更高版本:
    • 一旦到达冲突的声明/定义,A的编译将以错误终止B(即,对B的上述更改破坏了A以及任何其他滥用正向声明的客户端,而不是透明地工作)。
  • 否则(如果A最终没有包含b.h-如果A只是通过指针和/或引用在B周围存储/传递,则可能)
    • 依赖于#include分析和更改的文件时间戳的构建工具A在更改为B 后将不会重建(及其进一步相关的代码),从而在链接时或运行时导致错误。如果B是作为运行时加载的DLL分发的,则“ A”中的代码可能无法在运行时找到不同角度的符号,这些符号可能处理得不好或不足以触发有序关闭或可接受的缩减功能。

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


2
这是处理前向声明的一种真正干净的方法。唯一的“缺点”将在额外的文件中。我认为你总是包含a.fwd.ha.h,以确保它们保持同步。使用这些类的地方缺少示例代码。a.h并且b.h都需要包含在内,因为它们不会孤立运行:```//main.cpp #include“ ah” #include“ bh” int main(){...}```或其中之一需要像开头的问题一样完全包含在其他内容中。哪里b.h包含a.hmain.cpp包含b.h
Farway '17

2
@Farway在所有方面都对。我没有显示main.cpp,但很高兴您已在评论中记录了其中应包含的内容。干杯
Tony Delroy

1
更好的答案之一,其中有一个很好的详细解释,说明了由于优缺点而该做什么与​​不该做什么的原因
弗朗西斯·库格勒

1
@RezaHajianpour:对于要循环声明或不循环声明的所有类都具有一个向前声明标头是有意义的。就是说,您只会在以下情况下需要它们:1)包含实际声明的成本很高(或可以预期在以后变得昂贵)(例如,其中包含您的翻译部门可能不需要的很多标头),以及2)客户代码是可能能够使用指向对象的指针或引用。 <iosfwd>这是一个经典的示例:在许多地方可以引用一些流对象,并且<iostream>要包含很多对象。
托尼·德罗伊

1
@RezaHajianpour:我认为您有一个正确的主意,但是您的声明存在一个术语上的问题:“我们只需要声明该类型”就是正确的。被声明的类型意味着已经看到了前向声明。它的定义,一旦完整的定义已经被解析(以及您可能需要更多的#includeS)。
托尼·德罗伊

20

要记住的事情:

  • 如果class A对象为class B,则此方法无效,反之亦然。
  • 前向声明是要走的路。
  • 声明顺序很重要(这就是为什么要移出定义的原因)。
    • 如果两个类都调用另一个函数,则必须将定义移出。

阅读常见问题解答:


1
您提供的链接不再起作用,您是否碰巧知道要引用的新链接?
拉米亚饶

11

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

这样可以使两个(或多个)头文件中仍然有很多内联。但是有必要包括警卫队

像这样

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


为什么?我认为这是解决棘手问题的理想解决方案...当需要内联时。如果您不希望内联,则不应该像从一开始就编写代码一样……
epatel

如果用户B.h首先包含该怎么办?
福兹先生2014年

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


2

维基百科上提供的简单示例为我工作。(您可以在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。这些将仅包含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

然后,Ah和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的解决方案相比,这有什么优势吗?两者均基于“ helper”标头,但Tony的标头较小(它们仅包含前向声明),并且它们似乎以相同的方式工作(至少乍一看)。
Fabio说恢复莫妮卡

1
这个答案不能解决最初的问题。它只是说“将声明放入单独的标题中”。无需解决循环依赖关系(问题需要一个解决方案,其中可以使用A的和B定义,前向声明是不够的)。
geza

0

在某些情况下,可以在类A的头文件中定义类B的方法或构造函数,以解决涉及定义的循环依赖关系。这样,您可以避免将定义放入.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

该指令应延迟当前文件的处理并完成所有未决的包含。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.