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

发布于 2024-07-14 22:10:26 字数 1298 浏览 8 评论 0 原文

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

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


  • <前><代码>B类; A级 { int_val; B*_b; 民众: A(整数值) :_val(val) { } 无效 SetB(B *b) { _b = b; _b->打印(); // 编译器错误:C2027:使用未定义的类型“B” } 无效打印() { cout<<"类型:A val="<<_val<

  • Bh

    <前><代码>#include“啊” B级 { 双_val; A* _a; 民众: B(双值) :_val(val) { } 无效 SetA(A *a) { _a = a; _a->打印(); } 无效打印() { cout<<"类型:B val="<<_val<

  • main.cpp

    <前><代码>#include“Bh” #include ; int main(int argc, char* argv[]) { 一个(10); Bb(3.14); 打印(); a.SetB(&b); b.打印(); b.SetA(&a); 返回0; }

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.

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

秋凉 2024-07-21 22:10:26

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

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

// 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 {
  // 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 "Ah" 的人必须先声明 B 才能使用它,并且会得到一个可怕的 #包括错误。 因此,让我们将声明移至 Ah 本身。

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

而在Bh中,此时直接#include "Ah"即可。

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

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 .cc file (remember that the .cc and not the .h is 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 B then? Enough to store A! Oops.

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:

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

// 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 .cc looks like:

// 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 B is - it has never even seen the symbol before.

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

// 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 B before they can use it and will get a terrible #include error. So let's move the declaration into A.h itself.

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

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

分分钟 2024-07-21 22:10:26

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

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).

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.

//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;
}
乱世争霸 2024-07-21 22:10:26

我迟到了回答这个问题,但迄今为止还没有一个合理的答案,尽管这是一个热门问题,答案得到了高度赞扬......

最佳实践:前向声明标头

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

a.fwd.h:

#pragma once
class A;

ah:

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

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

b.fwd.h:

#pragma once
class B;

bh:

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

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

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

b.fwd.h:

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

bh:

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

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


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

说 - 不要使用上面解释的转发声明标头 - 使用 aha.cc 中的代码代替转发声明 < code>class B; 本身:

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

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

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:

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

The maintainers of the A and B libraries 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...

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.h and should complete cleanly.


Poor but common practice: forward declare stuff in other libs

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

  • if a.h or a.cc did include b.h later:
    • 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 #include analysis 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.

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

梦太阳 2024-07-21 22:10:26

需要记住的事情:

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

阅读常见问题解答:

Things to remember:

  • This won't work if class A has an object of class B as 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.

Read the FAQ:

客…行舟 2024-07-21 22:10:26

我曾经通过将所有内联移动到类定义之后并将其他类的#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__ */

......并在 Bh 中做同样的事情

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

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

笑着哭最痛 2024-07-21 22:10:26

我曾经写过一篇关于此的文章: 解决 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;
}

I've written a post about this once: Resolving circular dependencies in 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;
}
情徒 2024-07-21 22:10:26

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

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

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.

痴情换悲伤 2024-07-21 22:10:26

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

文件 '''ah''':

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

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 )

File '''a.h''':

#ifndef A_H
#define A_H

class B;    //forward declaration

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

File '''b.h''':

#ifndef B_H
#define B_H

class A;    //forward declaration

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

File '''main.cpp''':

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

int main() {
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}
心的憧憬 2024-07-21 22:10:26

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

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

  • 完全相同,
  • 解决方案与最初预期的内联函数
  • AB 的内联用户仍然可以包括Ah 和 Bh 以任意顺序

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

然后,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 是“私有”标头,AB 的用户不应使用它们。 公共标头是 Ah 和 Bh

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 A and B can include A.h and B.h in any order

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

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

// 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 A and B should not use them. The public header is A.h and B.h.

请远离我 2024-07-21 22:10:26

不幸的是我无法评论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

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

Unfortunately I can't comment the answer from geza.

He is not just saying "put forward declarations into a separate header". He says that you have to spilt class definition headers and inline function definitions into different header files to allow "defered dependencies".

But his illustration is not really good. Because both classes (A and B) only need an incomplete type of each other (pointer fields / parameters).

To understand it better imagine that class A has a field of type B not B*. In addition class A and B want to define an inline function with parameters of the other type:

This simple code would not work:

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

It would result in the following code:

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

This code does not compile because B::Do needs a complete type of A which is defined later.

To make sure that it compiles the source code should look like this:

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

This is exactly possible with these two header files for each class wich needs to define inline functions.
The only issue is that the circular classes can't just include the "public header".

To solve this issue I would like to suggest a preprocessor extension: #pragma process_pending_includes

This directive should defer the processing of the current file and complete all pending includes.

一袭水袖舞倾城 2024-07-21 22:10:26

首先我们需要一些定义。

定义

声明

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 中,因此应在单个 .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 B 无法定义,只能声明,并且 struct A 如下所示:

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

然后是 A 的实例化,而 A 的定义code>B 不可见(某些编译器可能不介意 B 是否稍后在同一 TU 中定义)将导致错误,因为 B 的默认构造函数以及析构函数code>A,导致生成unique_ptr的析构函数,需要B的定义[例如error: invalid application of 'sizeof' 为不完整类型 'B']。 不过,仍然有一种方法可以解决这个问题:不要使用生成的默认构造函数/析构函数。

例如,

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

将编译并且只有 A::A()A::~A() 两个未定义的符号,您仍然可以在定义之外内联编译它们像以前一样的 A (前提是您在执行此操作之前定义了 B)。

三个部分,三个文件?

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

  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(), CA::f( )CD::c() 在一些 .cpp 中定义。

但是,我们也可以内联它们; 此时我们需要定义 C 因为所有四个都需要它(B::BB::~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 定义标头的循环包含,即 Ah 包含 BhBh包括。 在这种情况下,您必须通过用指针替换类成员来解耦循环。

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

First we need a few definitions.

Definitions

Declaration

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

Definition

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

The difference is that repeating a definition causes a One Definition Rule (ODR) violation. The compiler will give an error along the lines of "error: redefinition of '...'".

Note that a "forward declaration" is just a declaration. Declarations can be repeated since they don't define anything and therefore cause no ODR.

Note that default arguments may only be given once, possibly during the declaration, but only for one of the declarations if there are multiple. Therefore one could argue that that is a definition because it may not be repeated (and in a sense it is: it defines the default arguments). However, since it doesn't define the function or template, lets call those a declaration anyway. Default arguments will be ignored below.

Function definitions

(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.

Non-inline, non-template (member) functions need to exist only in a single TU and should therefore be defined in a single .cpp.

However, inline- and/or template (member) functions are defined in headers, which might be included by multiple TU's, and therefore need special treatment by the linker. They too are considered to generate code however.

Class definitions

Class definitions might or might not generate code. If they do, then that is for functions that the linker will resolve any collisions of.

Of course, any member function that is defined inside the class is per definition "inline". If it is a problem that such a function is defined during the declaration of the class, it can simply be moved outside the class declaration.

Instead of,

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

do

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

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

Therefore we are mostly interested in code generation (function instantiations) that both, can not be moved outside the class declaration and requires some other definition in order to be instantiated.

It turns out that this usually involves smart pointers and default destructors. Assume that struct B can not be defined, only declared, and struct A looks as follows:

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

then an instantiation of A while the definition of B is not visible (some compilers might not mind if B is defined later in the same TU) will cause an error because both, the default constructor as well as the destructor of A, cause the destructor of unique_ptr<B> to be generated, which needs the definition of B [e.g. error: invalid application of ‘sizeof’ to incomplete type ‘B’]. There is still a way around this though: do not use generated default constructor/destructor.

For example,

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

will compile and just have two undefined symbols for A::A() and A::~A() which you can still compile inline outside of the definition of A as before (provided you define B before you do so).

Three parts, three files?

As such we can distinguish three part of a struct/class definition that we could each put in a different file.

  1. The (forward) declaration:

    A.fwd.h

  2. The class definition:

    A.h

  3. The inline and template member function definitions:

    A.inl.h

And then there is of course A.cpp with the non-inline and non-template member function definitions; but those are not relevant for circular header dependencies.

Ignoring default arguments, declarations won't require any other declaration or definition.

Class definitions might require certain other classes to be declared, yet others to be defined.

Inline/template member functions might require additional definitions.

We can therefore create the following example that show all possibilities:

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

where B::B(), B::~B(), C A::f() and C D::c() are defined in some .cpp.

But, lets inline those as well; at that point we need to define C because all four need that (B::B and B::~B because of the unique_ptr, see above). And doing so in this TU then suddenly makes it unnecessary to put B::B() and B::~B() outside of the definition of B (at least with the compiler that I am using). Nevertheless, lets keep B as it is.

Then we get:

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

In other words, the definition of A looks like this:

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

Note that in theory we could make multiple .inl.h headers: one for each function, if otherwise it drags in more than required and that causes a problem.

Forbidden patterns

Note that all #include's are at the top of all files.

(In theory) .fwd.h headers do not include other headers. Therefore they can be included at will and never lead to a circular dependency.

.h definition headers might include a .inl.h header, but if that leads to a circular header dependency then that can always be avoided by moving the function that uses the inlined function from that .inl.h to the .inl.h of the current class; in the case of smart pointers that might require to also move the destructor and/or constructor to that .inl.h.

Hence, the only remaining problem is a circular inclusion of .h definition headers, ie A.h includes B.h and B.h includes A.h. In that case you must decouple the loop by replacing a class member with a pointer.

Finally, it is not possible to have a loop of pure .inl.h files. If that is necessary you probably should move them to a single file in which case the compiler might or might not be able to solve the problem; but clearly you can't get ALL functions inlined when they use eachother, so you might as well manually decide which can be non-inlined.

奈何桥上唱咆哮 2024-07-21 22:10:26

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

In some cases it is possible to define a 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 .cc files, for example if you want to implement a header only library.

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文