ChatGPT解决这个技术问题 Extra ChatGPT

我什么时候可以使用前向声明?

我正在寻找何时允许在另一个类的头文件中对一个类进行前向声明的定义:

我是否可以为基类、作为成员的类、通过引用传递给成员函数的类等执行此操作?

我非常希望将其重命名为“我什么时候应该”,并且答案会适当更新...
@deworde当您说“应该”时,您是在征求意见。
@deworde 我的理解是,您希望尽可能使用前向声明,以缩短构建时间并避免循环引用。我能想到的唯一例外是包含文件包含 typedef,在这种情况下,在重新定义 typedef(并冒着更改的风险)和包含整个文件(连同其递归包含)之间需要权衡。
@OhadSchneider 从实际的角度来看,我不是我的标题的忠实粉丝。 ÷
基本上总是要求您包含不同的标头才能使用它们(构造函数参数的前向 decl 是这里的一大罪魁祸首)

L
Luc Touraille

把自己放在编译器的位置上:当你前向声明一个类型时,编译器只知道这个类型存在;它对其大小、成员或方法一无所知。这就是为什么它被称为不完全类型。因此,您不能使用该类型来声明成员或基类,因为编译器需要知道该类型的布局。

假设以下前向声明。

class X;

这是您可以做和不能做的事情。

你可以用不完整的类型做什么:

将成员声明为指向不完整类型的指针或引用:class Foo { X *p; X&r; };

声明接受/返回不完整类型的函数或方法:void f1(X); X f2();

定义接受/返回指向不完整类型的指针/引用(但不使用其成员)的函数或方法: void f3(X*, X&) {} X& f4() {} X* f5() {}

你不能用不完整的类型做什么:

将其用作基类 class Foo : X {} // 编译器错误!

用它来声明一个成员:class Foo { X m; // 编译器错误! };

使用此类型定义函数或方法 void f1(X x) {} // 编译器错误! X f2() {} // 编译器错误!

使用它的方法或字段,实际上试图取消引用类型不完整的变量 class Foo { X *m;无效方法() { m->someMethod(); // 编译器错误! int i = m->someField; // 编译器错误! } };

对于模板,没有绝对的规则:是否可以使用不完整的类型作为模板参数,取决于模板中使用该类型的方式。

例如,std::vector<T> 要求其参数是完整类型,而 boost::container::vector<T> 则不需要。有时,仅当您使用某些成员函数时才需要完整类型; this is the case for std::unique_ptr<T>,例如。

一个文档齐全的模板应该在其文档中指出其参数的所有要求,包括它们是否需要是完整的类型。


很好的答案,但请参阅下面的我不同意的工程点。简而言之,如果您不包含您接受或返回的不完整类型的标头,则您会强制对标头的使用者产生无形的依赖,而必须知道他们需要哪些其他类型。
@AndyDent:是的,但是标头的使用者只需要包含他实际使用的依赖项,因此这遵循“您只需为使用的内容付费”的 C++ 原则。但实际上,对于希望标头是独立的用户来说,这可能会带来不便。
这组规则忽略了一个非常重要的情况:您需要一个完整的类型来实例化标准库中的大多数模板。需要特别注意这一点,因为违反规则会导致未定义的行为,并且可能不会导致编译器错误。
+1 表示“将自己置于编译器的位置”。我想象“编译器”有胡子。
@JesusChrist:确切地说:当您按值传递对象时,编译器需要知道它的大小才能进行适当的堆栈操作;传递指针或引用时,编译器不需要对象的大小或布局,只需要地址的大小(即指针的大小),与指向的类型无关。
T
Timo Geusch

主要规则是您只能前向声明其内存布局(以及成员函数和数据成员)不需要在前向声明它的文件中知道的类。

这将排除基类以及通过引用和指针使用的类以外的任何东西。


几乎。您还可以在函数原型中将“普通”(即非指针/引用)不完整类型作为参数或返回类型引用。
我想用作我在头文件中定义的类的成员的类呢?我可以转发声明吗?
是的,但在这种情况下,您只能使用指向前向声明类的引用或指针。但是,它确实让您拥有成员。
M
Marc Mutz - mmutz

Lakos 区分类用法

in-name-only(前向声明就足够了)和 in-size(需要类定义)。

我从未见过它发音更简洁:)


什么是in-name-only 是什么意思?
@Boon:我敢说...?如果你只使用类的名字?
j
jxramos

除了指向不完整类型的指针和引用之外,您还可以声明指定参数和/或返回值的函数原型是不完整类型。但是,您不能定义具有不完整参数或返回类型的函数,除非它是指针或引用。

例子:

struct X;              // Forward declaration of X

void f1(X* px) {}      // Legal: can always use a pointer
void f2(X&  x) {}      // Legal: can always use a reference
X f3(int);             // Legal: return value in function prototype
void f4(X);            // Legal: parameter in function prototype
void f5(X) {}          // ILLEGAL: *definitions* require complete types

R
R Sahu

到目前为止,没有一个答案描述了何时可以使用类模板的前向声明。所以,就这样吧。

一个类模板可以转发声明为:

template <typename> struct X;

按照 accepted answer 的结构,

这是您可以做和不能做的事情。

你可以用不完整的类型做什么:

将成员声明为指向另一个类模板中不完整类型的指针或引用: template class Foo { X* ptr; X&ref; };

将成员声明为指向其不完整实例之一的指针或引用:class Foo { X* ptr; X&ref; };

声明接受/返回不完整类型的函数模板或成员函数模板:template void f1(X);模板 X f2();

声明接受/返回其不完整实例之一的函数或成员函数: void f1(X); X f2();

定义函数模板或成员函数模板,它们接受/返回指向不完整类型的指针/引用(但不使用其成员): template void f3(X*, X&) {} template < typename T> X& f4(X& in) { 返回; } 模板 X* f5(X* in) { return in; }

定义接受/返回指向其不完整实例之一的指针/引用的函数或方法(但不使用其成员): void f3(X*, X&) {} X& f4(X & in) { 返回; } X* f5(X* in) { 返回; }

将其用作另一个模板类的基类 template class Foo : X {} // OK,只要 X 在 // Foo 实例化之前定义。 Foo a1; // 编译器错误。模板 struct X {}; Foo a2; // 好的,因为现在定义了 X。

用它来声明另一个类模板的成员: template class Foo { X m; // 只要在实例化 Foo 之前定义了 X,就可以了。 }; Foo a1; // 编译器错误。模板 struct X {}; Foo a2; // 好的,因为现在定义了 X。

使用此类型定义函数模板或方法 template void f1(X x) {} // 如果在调用 f1 之前定义了 X 则 OK template X f2(){return X< T>(); } // 如果在调用 f2 void test1() { f1(X()); 之前定义了 X,则 OK // 编译器错误 f2(); // 编译器错误 } template struct X {};无效 test2() { f1(X()); // 好的,因为现在定义了 X f2(); // 好的,因为现在定义了 X }

你不能用不完整的类型做什么:

使用它的一个实例作为基类 class Foo : X {} // 编译器错误!

使用它的一个实例来声明一个成员:class Foo { X m; // 编译器错误! };

使用其实例之一定义函数或方法 void f1(X x) {} // 编译器错误! X f2() {返回 X(); } // 编译器错误!

使用其实例化之一的方法或字段,实际上试图取消引用类型不完整的变量 class Foo { X* m;无效方法() { m->someMethod(); // 编译器错误! int i = m->someField; // 编译器错误! } };

创建类模板模板 struct X 的显式实例化;


“到目前为止,没有一个答案描述了何时可以前向声明类模板。”这难道不是因为 XX<int> 的语义完全相同,只有前向声明的语法在任何实质性方面有所不同,除了 1 行之外的所有答案都等于只取 Luc 和 {3 }?这真的需要吗?还是我错过了一个不同的小细节?这是可能的,但我已经在视觉上比较了几次,看不到任何...
谢谢!该编辑添加了大量有价值的信息。我将不得不阅读它几次才能完全理解它......或者也许使用通常更好的策略,等到我对真实代码感到非常困惑并回到这里!我怀疑我将能够使用它来减少各个地方的依赖关系。
y
yesraaj

在您仅使用指向类的指针或引用的文件中。并且不应调用那些指针/引用的成员/成员函数。

with class Foo;//前向声明

我们可以声明 Foo* 或 Foo& 类型的数据成员。

我们可以用 Foo 类型的参数和/或返回值声明(但不能定义)函数。

我们可以声明 Foo 类型的静态数据成员。这是因为静态数据成员是在类定义之外定义的。


A
Andy Dent

我将其作为单独的答案而不仅仅是评论,因为我不同意 Luc Touraille 的答案,不是出于合法性,而是出于强大的软件和误解的危险。

具体来说,我对您希望界面用户必须知道的内容的隐含合同有疑问。

如果您要返回或接受引用类型,那么您只是说它们可以传递一个指针或引用,而这些指针或引用又可能只通过前向声明才知道。

当您返回不完整的类型 X f2(); 时,您是在说您的调用者必须具有 X 的完整类型规范。他们需要它才能在调用站点创建 LHS 或临时对象。

同样,如果您接受不完整的类型,则调用者必须已经构造了作为参数的对象。即使该对象作为函数的另一个不完整类型返回,调用站点也需要完整声明。 IE:

class X;  // forward for two legal declarations 
X returnsX();
void XAcceptor(X);

XAcepptor( returnsX() );  // X declaration needs to be known here

我认为有一个重要原则,即标头应提供足够的信息来使用它,而无需依赖其他标头。这意味着当您使用它声明的任何函数时,头文件应该能够包含在编译单元中而不会导致编译器错误。

除了

如果这种外部依赖是期望的行为。除了使用条件编译之外,您还可以有一个有据可查的要求,要求他们提供自己的标头声明 X。这是使用 #ifdefs 的替代方法,并且可以是引入模拟或其他变体的有用方法。重要的区别是一些模板技术,您明确不希望实例化它们,只是为了避免有人对我发脾气。


“我认为有一个重要原则,即标头应提供足够的信息来使用它,而无需依赖其他标头。” - Adrian McCarthy 在评论 Naveen 的回答时提到了另一个问题。这为不遵循“应该提供足够的信息以使用”原则提供了充分的理由,即使对于当前的非模板类型也是如此。
您正在谈论何时应该(或不应该)使用前向声明。不过,这完全不是这个问题的重点。这是关于了解(例如)想要打破循环依赖问题时的技术可能性。
I disagree with Luc Touraille's answer 所以给他写一条评论,如果你需要篇幅的话,还包括博客文章的链接。这没有回答所提出的问题。如果每个人都认为关于 X 是如何工作的问题有合理的答案,不同意 X 这样做,或者争论我们应该限制我们使用 X 的自由的限制——我们几乎没有真正的答案。
恕我直言,这个答案取决于开发人员的立场。例如:应用程序开发人员和库开发人员可能有不同的意见。
N
Naveen

我遵循的一般规则是不包含任何头文件,除非我必须这样做。因此,除非我将类的对象存储为我的类的成员变量,否则我不会包含它,我只会使用前向声明。


这会破坏封装并使代码变脆。为此,您需要知道类型是 typedef 还是具有默认模板参数的类模板的类,并且如果实现发生更改,则需要更新使用前向声明的任何地方。
@AdrianMcCarthy 是对的,一个合理的解决方案是拥有一个前向声明标头,该标头包含在其前向声明的内容的标头中,该标头也应由拥有该标头的任何人拥有/维护/运送。例如: iosfwd 标准库头,其中包含 iostream 内容的前向声明。
d
dirkgently

只要您不需要定义(想想指针和引用),您就可以摆脱前向声明。这就是为什么大多数情况下您会在标头中看到它们,而实现文件通常会为适当的定义提取标头。


P
Patrick Glandien

当您想使用其他类型(类)作为类的成员时,通常需要在类头文件中使用前向声明。您不能在头文件中使用前向声明的类方法,因为此时 C++ 还不知道该类的定义。这是您必须移入 .cpp 文件的逻辑,但如果您使用的是模板函数,则应将它们缩减为仅使用模板的部分并将该函数移至标题中。


这是没有意义的。不能有不完整类型的成员。任何类的声明都必须提供所有用户需要知道的关于其大小和布局的一切。它的大小包括其所有非静态成员的大小。前向声明一个成员会让用户不知道它的大小。
S
Sesh

假设前向声明将使您的代码编译(创建obj)。但是,除非找到定义,否则链接(exe 创建)将不会成功。


为什么有 2 个人对此投了赞成票?你不是在谈论问题在谈论什么。你的意思是正常的——不是前向的——函数的声明。问题是关于的前向声明。正如您所说的“前向声明将使您的代码编译”,帮我一个忙:编译 class A; class B { A a; }; int main(){},让我知道这是怎么回事。当然不会编译。这里所有正确的答案都解释了前向声明 有效的原因和精确的、有限的上下文。相反,你写的是完全不同的东西。
N
Niceman

我只想添加一件重要的事情,您可以使用 Luc Touraille 的答案中未提及的转发类来做。

你可以用不完整的类型做什么:

定义接受/返回指向不完整类型的指针/引用并将指针/引用转发给另一个函数的函数或方法。

void  f6(X*)       {}
void  f7(X&)       {}
void  f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }

一个模块可以将一个前向声明类的对象传递给另一个模块。


“转发的类”和“转发的声明的类”可能被误认为是指两个非常不同的东西。您所写的内容直接来自 Luc 的答案中隐含的概念,因此,虽然它会做出很好的评论,增加明确的说明,但我不确定它是否能证明答案的合理性。
d
double-beep

正如,Luc Touraille 已经很好地解释了在哪里使用和不使用类的前向声明。

我将补充一下为什么我们需要使用它。

我们应该尽可能使用前向声明来避免不必要的依赖注入。

由于 #include 头文件被添加到多个文件中,因此如果我们将头文件添加到另一个头文件中,它将在源代码的各个部分添加不需要的依赖注入,这可以通过将 #include 头文件添加到 .cpp 文件的任何位置来避免可能而不是添加到另一个头文件并在头 .h 文件中尽可能使用类前向声明。