我经常发现自己处于这样一种情况:由于一些糟糕的设计决策(由其他人做出:),我在c++项目中面临多个编译/链接器错误,这导致了不同头文件中c++类之间的循环依赖(也可能发生在同一个文件中)。但幸运的是,这种情况发生的次数并不多,所以当下次再次发生这种情况时,我还能记住解决这个问题的方法。

因此,为了便于以后回忆,我将发布一个有代表性的问题和解决方案。更好的解决方案当然是受欢迎的。


A.h B类; A类 { int _val; B * _b; 公众: (int val) : _val (val) { } SetB(B * B) { _b = b; _b - >打印();//编译错误:C2027:使用未定义的类型“B” } 无效的Print () { cout < <“类型:val = " < < _val < < endl; } };


B.h # include“A.h” B类 { 双_val; * _a; 公众: B(双val) : _val (val) { } SetA(A * A) { _a = a; _a - >打印(); } 无效的Print () { cout < <“B型:val = " < < _val < < endl; } };


main.cpp # include“B.h” # include < iostream > Int main(Int argc, char* argv[]) { 一个(10); B B (3.14); a.Print (); a.SetB(乙); b.Print (); b.SetA(和); 返回0; }


当前回答

我曾经写过一篇关于这个问题的文章:在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;
}

其他回答

在某些情况下,可以在类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();
}

下面是模板的解决方案:如何处理模板的循环依赖关系

解决这个问题的线索是在提供定义(实现)之前声明两个类。不能将声明和定义分离到单独的文件中,但是可以将它们作为单独的文件进行结构。

首先,我们需要一些定义。

定义

宣言

extern int n;
int f();
template<typename T> int g(T);
struct A;
template<typename T> struct B;

定义

int n;
int f() { return 42; }
template<typename T> int g(T) { return 42; }
struct A { int f(); };
template<typename T> struct B { int g(T*); };

不同之处在于重复定义会导致违反One definition Rule (ODR)。编译器会给出一个类似"error: redefinition of '…'"的错误。

注意,“向前声明”只是一种声明。声明可以重复,因为它们没有定义任何东西,因此不会导致ODR。

注意,默认参数只能给出一次,可能是在声明期间,但如果有多个声明,则只能给出其中一个。因此,有人可能会说这是一个定义,因为它可能不会被重复(在某种意义上它是:它定义了默认参数)。但是,由于它没有定义函数或模板,我们无论如何都将其称为声明。下面将忽略默认参数。

函数定义

(Member) function definitions generate code. Having multiple of those (in different Translation Units (TU's), otherwise you'd get an ODR violation already during compile time) normally leads to a linker error; except when the linker resolves the collision which it does for inline functions and templated functions. Both might or might not be inlined; if they are not 100% of the time inlined then a normal function (instantiation) needs to exist; that might cause the collision that I am talking about.

非内联、非模板(成员)函数只需要存在于单个TU中,因此应该在单个.cpp中定义。

然而,内联和/或模板(成员)函数定义在头文件中,可能被多个TU包含,因此需要链接器进行特殊处理。然而,它们也被认为是生成代码的。

类定义

类定义可能生成代码,也可能不生成代码。如果有,那是针对链接器将解决冲突的函数。

当然,在类内部定义的任何成员函数都是“内联”定义。如果在类声明期间定义了这样一个函数,那么可以简单地将它移到类声明之外。

相反的,

struct A {
  int f() const { return 42; }
};

do

struct A {
  inline int f() const;
}; // struct declaration ends here.

int A::f() const { return 42; }

因此,我们最感兴趣的是代码生成(函数实例化),它们不能被移到类声明之外,并且需要一些其他定义才能被实例化。

事实证明,这通常涉及智能指针和默认析构函数。假设结构B不能定义,只能声明,结构A如下所示:

struct B;
struct A { std::unique_ptr<B> ptr; };

那么A的实例化而B的定义不可见(一些编译器可能不介意稍后在同一TU中定义B)将导致错误,因为A的默认构造函数和析构函数都会生成unique_ptr<B>的析构函数,这需要B的定义[例如:' sizeof '对不完整类型' B '的无效应用]。不过,还是有办法解决这个问题:不要使用生成的默认构造函数/析构函数。

例如,

struct B;
struct A {
  A();
  ~A();
  std::unique_ptr<B> ptr;
};

将编译并只有两个未定义的符号A::A()和A::~A(),您仍然可以像以前一样在A的定义之外内联编译(前提是您在这样做之前定义了B)。

三个部分,三个文件?

因此,我们可以区分结构/类定义的三个部分,分别放在不同的文件中。

(向前)声明: A.fwd.h 类定义: A.h 内联和模板成员函数定义: A.inl.h

当然还有带有非内联和非模板成员函数定义的A.cpp;但这些与循环头依赖关系无关。

忽略默认参数,声明将不需要任何其他声明或定义。

类定义可能需要声明某些其他类,也可能需要定义其他类。

内联/模板成员函数可能需要其他定义。

因此,我们可以创建以下示例来展示所有可能性:

struct C;
struct B
{
  B();
  ~B();
  std::unique_ptr<C> ptr;  // Need declaration of C.
};

struct A
{
  B b;    // Needs definition of B.
  C f();  // Needs declaration of C.
};

inline A g()  // Needs definition of A.
{
  return {};
}

struct D
{
  A a = g();  // Needs definition of A.
  C c();      // Needs declaration of C.
};

B: B (), B:: ~ B (), C:: f()和C D:: C()定义一些. cpp。

但是,我们把它们也内联起来;在这一点上,我们需要定义C,因为这四个都需要(B::B和B::~B,因为unique_ptr,见上文)。在这个TU中这样做,突然就没有必要把B::B()和B::~B()放在B的定义之外(至少在我使用的编译器中)。尽管如此,我们保持B不变。

然后我们得到:

// C.fwd.h:
struct C;

// B.h:
struct B
{
  inline B();
  inline ~B();
  std::unique_ptr<C> ptr;
};

// A.h:
struct A
{
  B b;
  inline C f();
};

// D.h:
inline A g()
{
  return {};
}
struct D
{
  A a = g();
  inline C c();
};

// C.h:
struct C {};

// B.inl.h:
B::B() {}
B::~B() {}

// A.inl.h:
C A::f()
{
  D d;
  return d.c();
}

// D.inl.h:
C D::c()
{
  return {};
}

换句话说,A的定义是这样的

// A.fwd.h:
struct A;
// A.h:
#include "B.h"      // Already includes C.fwd.h, but well...
#include "C.fwd.h"  // We need C to be declared too.
struct A
{
  B b;
  inline C f();
};
// A.inl.h:
#include "A.h"
#include "C.h"
#include "D.inl.h"
C A::f()
{
  D d;
  return d.c();
}

请注意,理论上我们可以创建多个.inl.h头文件:每个函数一个头文件,如果不这样做,就会导致问题。

禁止模式

注意,所有#include都位于所有文件的顶部。

(理论上).fwd.h头文件不包括其他头文件。因此,可以随意包含它们,而不会导致循环依赖。

.h定义头文件可能包含.inl.h头文件,但如果这导致循环头文件依赖,那么总是可以通过将使用内联函数的函数从.inl.h移到当前类的.inl.h来避免这种情况;在智能指针的情况下,可能还需要将析构函数和/或构造函数移动到.inl.h。

因此,唯一剩下的问题是.h定义头的循环包含,即A.h包含B.h, B.h包含A.h。在这种情况下,必须通过用指针替换类成员来解耦循环。

最后,纯.inl.h文件的循环是不可能的。如果有必要,你可能应该把它们移动到一个文件中,在这种情况下,编译器可能无法解决问题;但显然,当它们相互使用时,你不能让所有函数都内联,所以你不妨手动决定哪些可以非内联。

需要记住的事情:

如果类A有类B的对象作为成员,这将不起作用,反之亦然。 向前申报是一种方式。 声明的顺序很重要(这就是为什么要移出定义)。 如果两个类都调用另一个类的函数,则必须将定义移出。

阅读常见问题:

如何创建两个相互了解的类? 对成员对象使用前向声明时需要特别注意什么? 前向声明与内联函数一起使用时需要特别注意什么?

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

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

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