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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/625799/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 16:22:40  来源:igfitidea点击:

Resolve build errors due to circular dependency amongst classes

c++compiler-errorscircular-dependencyc++-faq

提问by Autodidact

I often find myself in a situation where I am facing multiple compilation/linker errors in a C++ project due to some bad design decisions (made by someone else :) ) which lead to circular dependencies between C++ classes in different header files (can happen also in the same file). But fortunately(?) this doesn't happen often enough for me to remember the solution to this problem for the next time it happens again.

我经常发现自己在 C++ 项目中面临多个编译/链接器错误,因为一些糟糕的设计决策(由其他人:))导致不同头文件中的 C++ 类之间的循环依赖(也可能发生)在同一个文件中)。但幸运的是(?)这不会经常发生,让我记住下一次再次发生时这个问题的解决方案。

So for the purposes of easy recall in the future I am going to post a representative problem and a solution along with it. Better solutions are of-course welcome.

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



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

采纳答案by Roosh

The way to think about this is to "think like a compiler".

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

Imagine you are writing a compiler. And you see code like this.

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

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

When you are compiling the .ccfile (remember that the .ccand not the .his the unit of compilation), you need to allocate space for object A. So, well, how much space then? Enough to store B! What's the size of Bthen? Enough to store A! Oops.

当你编译.cc文件时(记住.cc而不是.h是编译单位),你需要为 object 分配空间A。那么,那么,有多少空间呢?够储存B!那么尺寸是B多少呢?够储存A!哎呀。

Clearly a circular reference that you must break.

显然,您必须打破循环引用。

You can break it by allowing the compiler to instead reserve as much space as it knows about upfront - pointers and references, for example, will always be 32 or 64 bits (depending on the architecture) and so if you replaced (either one) by a pointer or reference, things would be great. Let's say we replace in 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;
};

Now things are better. Somewhat. main()still says:

现在情况好多了。有些。main()仍然说:

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem

#include, for all extents and purposes (if you take the preprocessor out) just copies the file into the .cc. So really, the .cclooks like:

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

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

You can see why the compiler can't deal with this - it has no idea what Bis - it has never even seen the symbol before.

您可以看到为什么编译器不能处理这个问题——它不知道是什么B——它以前甚至从未见过这个符号。

So let's tell the compiler about B. This is known as a forward declaration, and is discussed further in this answer.

所以让我们告诉编译器关于B. 这称为前向声明,并在本答案中进一步讨论。

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

This works. It is not great. But at this point you should have an understanding of the circular reference problem and what we did to "fix" it, albeit the fix is bad.

有效。这不是很好。但是此时您应该了解循环引用问题以及我们为“修复”它所做的工作,尽管修复很糟糕。

The reason this fix is bad is because the next person to #include "A.h"will have to declare Bbefore they can use it and will get a terrible #includeerror. So let's move the declaration into A.hitself.

这个修复不好的原因是因为下一个人#include "A.h"必须B在他们可以使用它之前声明并且会得到一个可怕的#include错误。所以让我们把声明移到Ah本身。

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

And in B.h, at this point, you can just #include "A.h"directly.

而在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.

哈。

回答by Autodidact

You can avoid compilation errors if you remove the method definitions from the header files and let the classes contain only the method declarations and variable declarations/definitions. The method definitions should be placed in a .cpp file (just like a best practice guideline says).

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

The down side of the following solution is (assuming that you had placed the methods in the header file to inline them) that the methods are no longer inlined by the compiler and trying to use the inline keyword produces linker errors.

以下解决方案的缺点是(假设您已将方法放在头文件中以将它们内联)编译器不再内联这些方法,并且尝试使用 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;
}

回答by Tony Delroy

I'm late answering this, but there's not one reasonable answer to date, despite being a popular question with highly upvoted answers....

我迟到了回答这个问题,但迄今为止还没有一个合理的答案,尽管这是一个受欢迎的问题,得到了高度评​​价....

Best practice: forward declaration headers

最佳实践:前向声明标头

As illustrated by the Standard library's <iosfwd>header, the proper way to provide forward declarations for others is to have a forward declaration header. For example:

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

a.fwd.h:

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:

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

The maintainers of the Aand Blibraries should each be responsible for keeping their forward declaration headers in sync with their headers and implementation files, so - for example - if the maintainer of "B" comes along and rewrites the code to be...

AB库的维护者应该各自负责保持他们的前向声明标头与他们的标头和实现文件同步,因此 - 例如 - 如果“B”的维护者出现并将代码重写为......

b.fwd.h:

b.fwd.h:

template <typename T> class Basic_B;
typedef Basic_B<char> B;

b.h:

哈:

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

...then recompilation of the code for "A" will be triggered by the changes to the included b.fwd.hand should complete cleanly.

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



Poor but common practice: forward declare stuff in other libs

糟糕但常见的做法:在其他库中转发声明内容

Say - instead of using a forward declaration header as explained above - code in a.hor a.ccinstead forward-declares class B;itself:

说 - 而不是像上面解释的那样使用前向声明标头 - 代码在a.ha.cc代替前向声明class B;本身:

  • if a.hor a.ccdid include b.hlater:
    • compilation of A will terminate with an error once it gets to the conflicting declaration/definition of B(i.e. the above change to B broke A and any other clients abusing forward declarations, instead of working transparently).
  • otherwise (if A didn't eventually include b.h- possible if A just stores/passes around Bs by pointer and/or reference)
    • build tools relying on #includeanalysis and changed file timestamps won't rebuild A(and its further-dependent code) after the change to B, causing errors at link time or run time. If B is distributed as a runtime loaded DLL, code in "A" may fail to find the differently-mangled symbols at runtime, which may or may not be handled well enough to trigger orderly shutdown or acceptably reduced functionality.
  • 如果a.ha.cc确实包括b.h后来:
    • 一旦遇到冲突的声明/定义,A 的编译将因错误而终止B(即上述对 B 的更改破坏了 A 和任何其他滥用前向声明的客户端,而不是透明地工作)。
  • 否则(如果 A 最终没有包括b.h- 如果 A 只是通过指针和/或引用存储/传递 Bs,则可能)
    • 依赖于#include分析和更改的文件时间戳的构建工具A在更改为 B 后不会重建(及其进一步依赖的代码),从而在链接时或运行时导致错误。如果 B 作为运行时加载的 DLL 分发,则“A”中的代码可能无法在运行时找到不同修饰的符号,这可能会也可能不会被很好地处理以触发有序关闭或可接受的减少功能。

If A's code has template specialisations / "traits" for the old B, they won't take effect.

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

回答by dirkgently

Things to remember:

要记住的事情:

  • This won't work if class Ahas an object of class Bas a member or vice versa.
  • Forward declaration is way to go.
  • Order of declaration matters (which is why you are moving out the definitions).
    • If both classes call functions of the other, you have to move the definitions out.
  • 如果class A有一个对象class B作为成员,这将不起作用,反之亦然。
  • 前向声明是要走的路。
  • 声明的顺序很重要(这就是您移出定义的原因)。
    • 如果两个类都调用另一个类的函数,则必须将定义移出。

Read the FAQ:

阅读常见问题:

回答by epatel

I once solved this kind of problem by moving all inlinesafter the class definition and putting the #includefor the other classes just before the inlinesin the header file. This way one make sure all definitions+inlines are set prior the inlines are parsed.

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

Doing like this makes it possible to still have a bunch of inlines in both(or multiple) header files. But it's necessary to have include guards.

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

Like this

像这样

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

...and doing the same in B.h

...并在其中做同样的事情 B.h

回答by Eduard Wirch

I've written a post about this once: Resolving circular dependencies in c++

我曾经写过一篇关于这个的帖子:在 C++ 中解决循环依赖

The basic technique is to decouple the classes using interfaces. So in your case:

基本技术是使用接口对类进行解耦。所以在你的情况下:

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

回答by Tatyana

Here is the solution for templates: How to handle circular dependencies with templates

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

The clue to solving this problem is to declare both classes before providing the definitions (implementations). It's not possible to split the declaration and definition into separate files, but you can structure them as if they were in separate files.

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

回答by madx

The simple example presented on Wikipedia worked for me. (you can read the complete description at http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B)

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

File '''a.h''':

文件'''a.h''':

#ifndef A_H
#define A_H

class B;    //forward declaration

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

File '''b.h''':

文件'''b.h''':

#ifndef B_H
#define B_H

class A;    //forward declaration

class B {
public:
    A* a;
};
#endif //B_H

File '''main.cpp''':

文件'''main.cpp''':

#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}

回答by geza

Unfortunately, all the previous answers are missing some details. The correct solution is a little bit cumbersome, but this is the only way to do it properly. And it scales easily, handles more complex dependencies as well.

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

Here's how you can do this, exactly retaining all the details, and usability:

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

  • the solution is exactly the same as originally intended
  • inline functions still inline
  • users of Aand Bcan include A.h and B.h in any order
  • 解决方案与最初预期的完全相同
  • 内联函数仍然内联
  • 的用户AB可以包括阿和bh以任何顺序

Create two files, A_def.h, B_def.h. These will contain only A's and B's definition:

创建两个文件,A_def.h、B_def.h。这些将仅包含A's 和B's 定义:

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

And then, A.h and B.h will contain this:

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

Note that A_def.h and B_def.h are "private" headers, users of Aand Bshould not use them. The public header is A.h and B.h.

需要注意的是A_def.h和B_DEF.H是“私有”报头,用户AB不能使用它们。公共标题是 Ah 和 Bh

回答by jkoendev

In some cases it is possible to definea method or a constructor of class B in the header file of class A to resolve circular dependencies involving definitions. In this way you can avoid having to put definitions in .ccfiles, for example if you want to implement a header only library.

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