提问人:Sandeep Datta 提问时间:3/9/2009 最后编辑:StoryTeller - Unslander MonicaSandeep Datta 更新时间:12/10/2022 访问量:298808
解决由于类之间的循环依赖关系而导致的生成错误
Resolve build errors due to circular dependency amongst classes
问:
我经常发现自己在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; }
答:
如果从头文件中删除方法定义,并让类仅包含方法声明和变量声明/定义,则可以避免编译错误。方法定义应放在 .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;
}
评论
要记住的事情:
- 如果将对象作为成员,这将不起作用,反之亦然。
class A
class B
- 远期申报是要走的路。
- 声明的顺序很重要(这就是您移出定义的原因)。
- 如果两个类都调用另一个类的函数,则必须将定义移出。
阅读常见问题解答:
评论
思考这个问题的方法是“像编译器一样思考”。
想象一下,你正在编写一个编译器。你会看到这样的代码。
// 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 分配空间。那么,那么,有多少空间呢?足够存储!那么规模有多大?足够存储!哎呀。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;
};
现在情况好多了。有点。 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。
评论
A
B
invalid use of incomplete type B in class A
我曾经通过将所有内联移动到类定义之后并将其他类放在头文件中的内联之前来解决此类问题。这样,就可以确保在解析内联之前设置了所有定义+内联。#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
评论
B.h
我曾经写过一篇关于这个的文章:在 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;
}
评论
virtual
维基百科上提供的简单示例对我有用。 (您可以在 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;
}
我回答这个问题很晚,但迄今为止没有一个合理的答案,尽管这是一个热门的问题,答案很高......
最佳做法:正向声明标头
如标准库的标头所示,为其他人提供正向声明的正确方法是具有正向声明标头。例如:<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” 的维护者出现并将代码重写为......A
B
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.h
a.cc
class B;
- 如果或确实包括以后:
a.h
a.cc
b.h
- 一旦 A 的编译达到冲突的声明/定义,就会因错误而终止(即上述对 B 的更改破坏了 A 和任何其他客户端滥用正向声明,而不是透明地工作)。
B
- 一旦 A 的编译达到冲突的声明/定义,就会因错误而终止(即上述对 B 的更改破坏了 A 和任何其他客户端滥用正向声明,而不是透明地工作)。
- 否则(如果 A 最终没有包含 - 如果 A 只是通过指针和/或引用存储/传递 B)
b.h
- 依赖于分析和更改的文件时间戳的生成工具在更改为 B 后不会重新生成(及其进一步依赖的代码),从而在链接时或运行时导致错误。如果 B 作为运行时加载的 DLL 分发,则“A”中的代码可能无法在运行时找到不同修改的符号,这些符号可能处理得足够好,也可能不够好,无法触发有序关闭或可接受的功能减少。
#include
A
- 依赖于分析和更改的文件时间戳的生成工具在更改为 B 后不会重新生成(及其进一步依赖的代码),从而在链接时或运行时导致错误。如果 B 作为运行时加载的 DLL 分发,则“A”中的代码可能无法在运行时找到不同修改的符号,这些符号可能处理得足够好,也可能不够好,无法触发有序关闭或可接受的功能减少。
如果 A 的代码具有旧代码的模板专用化/“特征”,则它们不会生效。B
评论
a.fwd.h
a.h
a.h
b.h
b.h
a.h
main.cpp
b.h
main.cpp
<iosfwd>
<iostream>
#include
以下是模板的解决方案: 如何使用模板处理循环依赖关系
解决此问题的线索是在提供定义(实现)之前声明这两个类。无法将声明和定义拆分为单独的文件,但可以像在单独的文件中一样构建它们。
不幸的是,之前的所有答案都缺少一些细节。正确的解决方案有点麻烦,但这是正确完成的唯一方法。它可以轻松扩展,也可以处理更复杂的依赖关系。
以下是执行此操作的方法,完全保留所有细节和可用性:
- 解决方案与最初预期完全相同
- 内联函数仍内联
- 和 可以按任意顺序包含 A.h 和 B.h 的用户
A
B
创建两个文件,A_def.h B_def.h。它们将仅包含 和 的定义:A
B
// 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。A
B
评论
A
B
x_def.h
x.hpp
x.h
x.cpp
在某些情况下,可以在类 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();
}
不幸的是,我无法评论 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
此指令应延迟当前文件的处理并完成所有挂起的包含。
首先,我们需要一些定义。
定义
声明
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 B
struct A
struct B;
struct A { std::unique_ptr<B> ptr; };
那么 while 的实例化定义是不可见的(一些编译器可能不介意后面是否在同一个 TU 中定义)将导致错误,因为默认构造函数和 的析构函数都会导致生成析构函数,这需要定义 [例如]。不过,仍然有一种方法可以解决这个问题:不要使用生成的默认构造函数/析构函数。A
B
B
A
unique_ptr<B>
B
error: 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()
A
B
三个部分,三个文件?
因此,我们可以区分结构/类定义的三个部分,每个部分都可以放在不同的文件中。
(转发)声明:
A.fwd.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 A::f()
C D::c()
.cpp
但是,让我们也内联这些;在这一点上,我们需要定义,因为所有四个都需要它(并且因为 ,见上文)。然后,在这个 TU 中这样做会突然变得没有必要将 和 排除在定义之外(至少在我使用的编译器中)。尽管如此,让我们保持原样。C
B::B
B::~B
unique_ptr
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
因此,唯一剩下的问题是循环包含定义标头,即 includes 和 includes 。在这种情况下,必须通过将类成员替换为指针来解耦循环。.h
A.h
B.h
B.h
A.h
最后,不可能有纯文件的循环。如果有必要,您可能应该将它们移动到单个文件中,在这种情况下,编译器可能无法解决问题;但显然,当它们相互使用时,您无法让所有函数内联,因此您不妨手动决定哪些函数可以是非内联的。.inl.h
上一个:什么是复制和交换成语?
评论