433

我经常发现自己在一个 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;
    }
    
4

11 回答 11

350

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

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

// 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。那么,那么,有多少空间呢?够收纳B!那它的大小是B多少?够收纳A!哎呀。

显然是一个你必须打破的循环引用。

您可以通过允许编译器保留尽可能多的空间来打破它,例如,指针和引用将始终为 32 位或 64 位(取决于体系结构),因此如果您替换(其中一个)为一个指针或参考,事情会很棒。假设我们替换 in 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。

于 2009-03-09T21:15:24.577 回答
115

如果从头文件中删除方法定义并让类仅包含方法声明和变量声明/定义,则可以避免编译错误。方法定义应该放在一个 .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;
}
于 2009-03-09T10:57:46.980 回答
42

我迟到了回答这个问题,但到目前为止还没有一个合理的答案,尽管它是一个受欢迎的问题,答案非常受欢迎......

最佳实践:前向声明标头

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

:

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

:

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

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


糟糕但常见的做法:前向声明其他库中的内容

说 - 而不是使用前面解释的前向声明标题 - 代码中a.ha.cc改为前向声明class B;自身:

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

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

于 2015-03-23T11:53:12.730 回答
23

要记住的事情:

  • class A如果有一个对象作为成员,这将不起作用,class B反之亦然。
  • 前向声明是要走的路。
  • 声明顺序很重要(这就是您要移出定义的原因)。
    • 如果两个类都调用另一个类的函数,则必须将定义移出。

阅读常见问题解答:

于 2009-03-09T11:07:12.400 回答
15

我曾经通过在类定义之后移动所有内并将其他类放在头文件中的内#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

于 2009-03-09T21:25:01.637 回答
7

我曾经写过一篇关于这个的帖子: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;
}
于 2013-12-15T18:12:47.900 回答
6

这是模板的解决方案:如何使用模板处理循环依赖项

解决这个问题的线索是在提供定义(实现)之前声明这两个类。无法将声明和定义拆分为单独的文件,但您可以将它们构造为就像它们位于单独的文件中一样。

于 2015-10-09T21:21:42.160 回答
4

Wikipedia 上提供的简单示例对我有用。(您可以在http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B阅读完整的描述)

文件'''啊''':

#ifndef A_H
#define A_H

class B;    //forward declaration

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

文件'''bh''':

#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;
}
于 2014-12-10T16:48:17.100 回答
2

不幸的是,所有以前的答案都缺少一些细节。正确的解决方案有点麻烦,但这是正确执行此操作的唯一方法。它可以轻松扩展,也可以处理更复杂的依赖关系。

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

  • 该解决方案与最初的预期完全相同
  • 内联函数仍然是内联的
  • 的用户A并且B可以按任意顺序包括 Ah 和 Bh

创建两个文件,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

然后, 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

于 2018-07-05T07:02:16.567 回答
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();
}

于 2019-03-30T22:00:10.927 回答
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

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

于 2019-04-24T22:09:02.917 回答