解决由于类之间的循环依赖关系而导致的生成错误

Resolve build errors due to circular dependency amongst classes

提问人:Sandeep Datta 提问时间:3/9/2009 最后编辑:StoryTeller - Unslander MonicaSandeep Datta 更新时间:12/10/2022 访问量:298808

问:

我经常发现自己在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;
    }
    
C 编译器错误 循环依赖 C++-FAQ

评论

30赞 wip 9/12/2012
使用 Visual Studio 时,/showIncludes 标志对调试此类问题有很大帮助。
0赞 Erik 11/9/2021
Visual Studio Code 是否有类似的东西?

答:

120赞 Sandeep Datta 3/9/2009 #1

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

评论

0赞 Lenar Hoyt 10/5/2014
谢谢。这很容易解决了问题。我只是将循环包含移动到 .cpp 文件中。
7赞 Malcolm 9/1/2016
如果您有模板方法怎么办?然后,除非您手动实例化模板,否则您无法真正将其移动到 CPP 文件中。
0赞 Gusev Slava 9/30/2018
您始终将“A.h”和“B.h”放在一起。为什么不在“B.h”中包含“A.h”,然后在“A.cpp”和“B.cpp”中仅包含“B.h”?
0赞 HanniBaL90 12/22/2020
谢谢,对于那些需要 2 个类之间的这种相互依赖关系并且无法以不同方式重构它的人来说,这是一个很好的答案
26赞 dirkgently 3/9/2009 #2

要记住的事情:

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

阅读常见问题解答:

评论

2赞 Ramya Rao 2/23/2017
您提供的链接不再起作用,您碰巧知道要参考的新链接吗?
0赞 Welgriv 1/4/2023
@RamyaRao这里 : isocpp.org/wiki/faq/misc-technical-issues#forward-decl-members (你只需要向上滚动一点)
382赞 Roosh 3/10/2009 #3

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

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

// 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 分配空间。那么,那么,有多少空间呢?足够存储!那么规模有多大?足够存储!哎呀。ABBA

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

你可以通过允许编译器保留尽可能多的空间来破坏它 - 例如,指针和引用将始终是 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;
};

现在情况好多了。有点。 still 说道: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;
}

行得通。这不是很好。但是在这一点上,您应该了解循环引用问题以及我们为“修复”它所做的工作,尽管修复很糟糕。

这个修复程序不好的原因是,下一个人必须声明才能使用它,并且会得到一个可怕的错误。因此,让我们将声明移到 A.h 本身。#include "A.h"B#include

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

而在B.h.中,在这一点上,你可以直接。#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。

评论

26赞 Peter Ajtai 11/17/2010
“告诉编译器 B 有关”称为 B 的正向声明。
14赞 kellogs 11/7/2011
我的天啊!完全忽略了一个事实,即参考文献在占用空间方面是已知的。终于,现在我可以正确地设计了!
69赞 rank1 4/17/2013
但是您仍然不能在 B 上使用任何函数(如问题 _b->Printt())
4赞 Ben Voigt 4/11/2015
@sydan:你不能。解析循环依赖关系需要类外定义
6赞 Silidrone 9/5/2017
但是我需要在类 B 中用作完整类型,在类 A 中用作完整类型。我所说的完整类型,是指从该类型的对象调用函数。我该怎么做?我只是得到错误,.ABinvalid use of incomplete type B in class A
15赞 epatel 3/10/2009 #4

我曾经通过将所有内联移动到类定义之后并将其他类放在头文件中的内联之前来解决此类问题。这样,就可以确保在解析内联之前设置了所有定义+内联。#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

评论

0赞 epatel 3/11/2009
为什么?我认为这是解决棘手问题的优雅解决方案......当人们想要内联时。如果一个人不想要内联,就不应该像从一开始就写代码一样编写代码......
0赞 Mr Fooz 3/19/2014
如果用户首先包含,会发生什么情况?B.h
3赞 Lars Viklund 8/10/2015
请注意,标头防护使用的是保留标识符,任何带有双相邻下划线的内容都会被保留。
8赞 Eduard Wirch 12/16/2013 #5

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

评论

4赞 Colin Emonds 6/23/2016
请注意,使用接口会影响运行时性能。virtual
4赞 madx 12/11/2014 #6

维基百科上提供的简单示例对我有用。 (您可以在 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;
}
49赞 Tony Delroy 3/23/2015 #7

我回答这个问题很晚,但迄今为止没有一个合理的答案,尽管这是一个热门的问题,答案很高......

最佳做法:正向声明标头

如标准库的标头所示,为其他人提供正向声明的正确方法是具有正向声明标头。例如:<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;

B.H:

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

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

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;

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


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

比如说 - 而不是使用上面解释的正向声明标头 - 代码 in 或 forward-de声明自己:a.ha.ccclass B;

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

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

评论

2赞 Farway 5/6/2017
这是处理前向声明的一种非常干净的方法。唯一的“缺点”是额外的文件。我假设您始终包含在 中,以确保它们保持同步。使用这些类的地方缺少示例代码。 并且两者都需要包括在内,因为它们不会单独运行: ''' //main.cpp #include “a.h” #include “b.h” int main() { ... } ''' 或者其中一个需要完全包含在另一个中,就像在开头的问题中一样。其中包括和包括a.fwd.ha.ha.hb.hb.ha.hmain.cppb.h
2赞 Tony Delroy 5/6/2017
@Farway 在所有方面都是对的。我没有费心展示,但很高兴你已经在评论中记录了它应该包含的内容。干杯main.cpp
1赞 Francis Cugler 1/16/2018
更好的答案之一,很好地详细解释了为什么由于利弊而该做和不该做......
1赞 Tony Delroy 1/23/2019
@RezaHajianpour:对于所有想要正向声明的类,无论是否循环,都有一个正向声明标头是有意义的。也就是说,只有在以下情况下,您才会需要它们:1) 包含实际声明是(或可以预期以后会变得)成本高昂(例如,它包含许多您的翻译单元可能不需要的标头),以及 2) 客户端代码可能能够使用对对象的指针或引用。 是一个典型的例子:可以从许多地方引用一些流对象,并且要包含很多内容。<iosfwd><iostream>
1赞 Tony Delroy 1/25/2019
@RezaHajianpour:我认为你的想法是正确的,但你的说法存在术语问题:“我们只需要声明的类型”是正确的。声明的类型表示已看到正向声明;一旦解析了完整的定义,它就会被定义为此,您可能需要更多 s)。#include
8赞 Tatyana 10/10/2015 #8

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

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

3赞 geza 7/5/2018 #9

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

以下是执行此操作的方法,完全保留所有细节和可用性:

  • 解决方案与最初预期完全相同
  • 内联函数仍内联
  • 和 可以按任意顺序包含 A.h 和 B.h 的用户AB

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

然后,A.h 和 B.h 将包含以下内容:

// 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.h 和 B.h。AB

评论

1赞 Fabio says Reinstate Monica 11/20/2018
Tony Delroy的解决方案相比,这有什么优势吗?两者都基于“helper”标头,但 Tony 的更小(它们只包含 forward 声明),并且它们似乎以相同的方式工作(至少乍一看)。
1赞 geza 11/20/2018
这个答案并不能解决最初的问题。它只是说“将声明放入单独的标头中”。与解决循环依赖关系无关(该问题需要一个解决方案,其中 ' 和 定义可用,前向声明是不够的)。AB
0赞 Welgriv 1/4/2023
你的 s 真的看起来像普通的 s,就像你的 s 真的看起来像 s ......x_def.hx.hppx.hx.cpp
0赞 jkoendev 3/31/2019 #10

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

1赞 Bernd 4/25/2019 #11

不幸的是,我无法评论 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::D o 需要稍后定义的完整类型 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

此指令应延迟当前文件的处理并完成所有挂起的包含。

1赞 Carlo Wood 8/10/2022 #12

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

定义

声明

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

区别在于,重复定义会导致违反单一定义规则 (ODR)。编译器将给出一个 “” 行的错误。error: redefinition of '...'

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

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

函数定义

(成员) 函数定义生成代码。拥有多个这样的转换单元(在不同的转换单元 (TU) 中,否则您将在编译时收到 ODR 冲突)通常会导致链接器错误;除非链接器解析冲突,它对内联函数和模板化函数执行此操作。两者都可能是内联的,也可能不是内联的;如果它们不是 100% 的内联时间,则需要存在正常函数(实例化);这可能会导致我所说的碰撞。

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

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

类定义

类定义可能会生成代码,也可能不会生成代码。如果它们这样做,则适用于链接器将解决任何冲突的函数。

当然,在类中定义的任何成员函数都是根据定义“内联”的。如果在类声明期间定义了这样的函数有问题,则可以简单地将其移到类声明之外。

而不是

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

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

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

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

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

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

那么 while 的实例化定义是不可见的(一些编译器可能不介意后面是否在同一个 TU 中定义)将导致错误,因为默认构造函数和 的析构函数都会导致生成析构函数,这需要定义 [例如]。不过,仍然有一种方法可以解决这个问题:不要使用生成的默认构造函数/析构函数。ABBAunique_ptr<B>Berror: invalid application of ‘sizeof’ to incomplete type ‘B’

例如

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

将编译并且只有两个未定义的符号,并且您仍然可以在 As Previous 的定义之外进行内联编译(前提是您在这样做之前定义)。A::A()A::~A()AB

三个部分,三个文件?

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

  1. (转发)声明:

    A.fwd.h

  2. 类定义:

  3. 内联和模板成员函数定义:

    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 A::f()C D::c().cpp

但是,让我们也内联这些;在这一点上,我们需要定义,因为所有四个都需要它(并且因为 ,见上文)。然后,在这个 TU 中这样做会突然变得没有必要将 和 排除在定义之外(至少在我使用的编译器中)。尽管如此,让我们保持原样。CB::BB::~Bunique_ptrB::B()B::~B()BB

然后我们得到:

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

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

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