解决由类之间循环依赖引起的构建错误

8 浏览
0 Comments

解决由类之间循环依赖引起的构建错误

我经常发现自己面临着C++项目中多个编译器/链接器错误的情况,这是由于一些糟糕的设计决策(由他人做出的 :))导致了不同头文件中C++类之间的循环依赖(在同一个文件中也可能发生)。但幸运的是(?)这种情况并不经常发生,以至于我无法记住下次再次发生时的解决方案。\n因此,为了方便以后的回忆,我打算发布一个典型问题及其解决方案。当然,更好的解决方案是受欢迎的。\n---\nA.h\n

class B;
class A
{
    int _val;
    B *_b;
public:
    A(int val)
        :_val(val)
    {
    }
    void SetB(B *b)
    {
        _b = b;
        _b->Print(); // 编译器错误:C2027:使用未定义的类型'B'
    }
    void Print()
    {
        cout<<"类型:A 值="<<_val<

\n---\nB.h\n

#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<<"类型:B 值="<<_val<

\n---\nmain.cpp\n

#include "B.h"
#include 
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;
}

0
0 Comments

解决由类之间循环依赖引起的构建错误

在解决类之间循环依赖引起的构建错误时,有几种最佳实践和常见的做法。

最佳实践:使用前向声明头文件

前向声明头文件是为了提供给其他类的前向声明的正确方式。例如:

a.fwd.h:

#pragma once
class A;

a.h:

#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*);
};

"A"和"B"库的维护者应负责让他们的前向声明头文件与其头文件和实现文件保持同步。例如,如果"B"的维护者重新编写代码,如下所示:

b.fwd.h:

template  class Basic_B;
typedef Basic_B B;

b.h:

template 
class Basic_B
{
    ...class definition...
};
typedef Basic_B B;

则对"A"代码的重新编译将由于对包含的b.fwd.h的更改而触发,并且应该能够顺利完成。

常见但不推荐的做法:在其他库中前向声明

假设在a.h或a.cc中,而不是按照上述方式使用前向声明头文件,代码直接在a.h或a.cc中使用class B;进行前向声明:

- 如果a.h或a.cc后来包含了b.h:

- 一旦编译到冲突的B声明/定义(即上面对B的更改破坏了A和其他滥用前向声明的客户端的工作透明性)时,A的编译将以错误终止。

- 否则(如果A最终没有包含b.h - 如果A只是通过指针和/或引用存储/传递B):

- 依赖于#include分析和已更改文件时间戳的构建工具将在对B的更改后不会重新构建A(及其进一步依赖的代码),导致链接时或运行时出现错误。如果B被分发为运行时加载的DLL,"A"中的代码可能无法在运行时找到不同名称的符号,这可能或可能不会被很好地处理,从而触发有序关闭或可接受的功能降级。

如果A的代码对旧的B有模板特化/“特性”,它们将不起作用。

这是一种非常干净的处理前向声明的方式。唯一的缺点可能是需要额外的文件。我假设您始终在a.h中包含a.fwd.h,以确保它们保持同步。示例代码中缺少了使用这些类的地方。a.h和b.h都需要被包含,因为它们无法独立运行:

//main.cpp
#include "a.h"
#include "b.h"
int main() {
    ...
}

或者其中一个完全包含另一个,就像在开头的问题中所示。其中b.h包含a.h,而main.cpp包含b.h。

以上都正确。我没有显示main.cpp,但很高兴你在评论中记录了它应该包含的内容。谢谢。

这是一个很好的答案,详细解释了为什么要这样做以及由于利弊而不应该这样做的事情。

根据这个解决方案,我们是否需要为每个类都有一个单独的Class.fwd.h文件?还是只需要在可能存在循环依赖的类中提供前向声明即可?

- 对于所有需要前向声明的类,无论是否存在循环依赖,都有意义。但是,只有在以下情况下才需要使用它们:

1. 包含实际声明会(或可能会)变得昂贵(例如,它包含许多您的翻译单元可能不需要的头文件)。

2. 客户端代码可能能够使用指针或引用来访问这些对象。

``就是一个经典的例子:有几个流对象被许多地方引用,而``的包含量很大。

因此,我们可以只包含前向声明头文件,只需要类型被定义并位于范围内,而无需包含更多头文件,对吗?

- 我认为你的理解是正确的,但是你的陈述存在一个术语问题:“我们只需要类型被声明”是正确的。类型被声明意味着前向声明已经被看到;它在完全定义被解析后被定义(对于这一点,您可能需要更多的#include)。

我希望这个答案能够得到更多关注和足够的点赞,以便能够成为重点和核心。因为与最高分的答案相比,这个答案的分数差距超过230分,因此不会得到关注。

0
0 Comments

解决由于类之间存在循环依赖而引起的构建错误

当类之间存在循环依赖时,编译错误可以通过从头文件中移除方法定义并将方法声明和变量声明/定义放在类中来避免。方法定义应该放在一个.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 
using namespace std;
A::A(int val)
:_val(val)
{
}
void A::SetB(B *b)
{
    _b = b;
    cout<<"Inside SetB()"<Print();
}
void A::Print()
{
    cout<<"Type:A val="<<_val<
using namespace std;
B::B(double val)
:_val(val)
{
}
void B::SetA(A *a)
{
    _a = a;
    cout<<"Inside SetA()"<Print();
}
void B::Print()
{
    cout<<"Type:B val="<<_val<

谢谢。这个解决方案很容易解决问题。我只需要将循环包含的部分移到.cpp文件中。

如果你有一个模板方法怎么办?除非手动实例化模板,否则你不能真正将它移到一个CPP文件中。

你总是一起包含"A.h"和"B.h"。为什么不在"B.h"中包含"A.h",然后在"A.cpp"和"B.cpp"中只包含"B.h"呢?

谢谢,对于那些需要这两个类之间相互依赖并且无法以不同方式重构的人来说,这是一个很好的答案。

0
0 Comments

解决由类之间的循环依赖引起的构建错误

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

// 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 {
  // 这两个都可以,同样适用于各种const版本。
  B& _b_ref;
  B* _b_ptr;
};

现在情况有所改善。在main()中仍然有:

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem
int main (...) {
  A a;
}

#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错误。所以让我们将声明移到A.h本身。

// file: A.h
class B;
class A {
  B* _b; // 或其他变体。
};

此时,在B.h中,您可以直接包含#include "A.h"。

// file: B.h
#include "A.h"
class B {
  // 请注意,这很好,因为编译器在这个时候知道A将需要多少空间。
  A _a; 
}

HTH。

"告诉编译器关于B"被称为B的前向声明。

天呐!完全忽略了引用是以占用空间为单位。现在,我可以正确地进行设计了!

但仍然无法在B上使用任何函数(如问题中的_b->Printt())

这是我遇到的问题。如何在前向声明中引入函数而不完全重写头文件?

:你不能。解决循环依赖需要在类外部定义。

但是我需要在A类中将B用作完整类型,并且在B类中将A用作完整类型。所谓完整类型,是指从该类型的对象调用函数。我该怎么做?我只得到错误,类A中使用不完整类型B。

为什么编译器不能绕过循环依赖,即sizeofA = sizeof(A) - 对b的引用:sizeofB = sizeof(B) - 对a的引用:realSzA = realSzB =(sizeofA + sizeOfB)

:Sandeep Datta下面的答案解决了这个问题。在头文件中进行类的前向声明,并在两个.cpp文件中包含两个头文件。(这是2个前向声明和4个包含文件。)

在“A.h”中前向声明类B - >在“B.h”中包含“A.h”,然后只在“A.cpp”和“B.cpp”中包含“B.h”。 (这是1个前向声明和3个包含文件。)

请注意,对于引用成员是否实际需要存储,这是未指定的-由于不能取到其地址,因此没有合法的方法来判断。

0