我正在寻找何时允许在另一个类的头文件中对一个类进行前向声明的定义:
我是否可以为基类、作为成员的类、通过引用传递给成员函数的类等执行此操作?
把自己放在编译器的位置上:当你前向声明一个类型时,编译器只知道这个类型存在;它对其大小、成员或方法一无所知。这就是为什么它被称为不完全类型。因此,您不能使用该类型来声明成员或基类,因为编译器需要知道该类型的布局。
假设以下前向声明。
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>
,例如。
一个文档齐全的模板应该在其文档中指出其参数的所有要求,包括它们是否需要是完整的类型。
主要规则是您只能前向声明其内存布局(以及成员函数和数据成员)不需要在前向声明它的文件中知道的类。
这将排除基类以及通过引用和指针使用的类以外的任何东西。
Lakos 区分类用法
in-name-only(前向声明就足够了)和 in-size(需要类定义)。
我从未见过它发音更简洁:)
除了指向不完整类型的指针和引用之外,您还可以声明指定参数和/或返回值的函数原型是不完整类型。但是,您不能定义具有不完整参数或返回类型的函数,除非它是指针或引用。
例子:
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
到目前为止,没有一个答案描述了何时可以使用类模板的前向声明。所以,就这样吧。
一个类模板可以转发声明为:
template <typename> struct X;
按照 accepted answer 的结构,
这是您可以做和不能做的事情。
你可以用不完整的类型做什么:
将成员声明为指向另一个类模板中不完整类型的指针或引用: template
将成员声明为指向其不完整实例之一的指针或引用:class Foo { X
声明接受/返回不完整类型的函数模板或成员函数模板:template
声明接受/返回其不完整实例之一的函数或成员函数: void f1(X
定义函数模板或成员函数模板,它们接受/返回指向不完整类型的指针/引用(但不使用其成员): template
定义接受/返回指向其不完整实例之一的指针/引用的函数或方法(但不使用其成员): void f3(X
将其用作另一个模板类的基类 template
用它来声明另一个类模板的成员: template
使用此类型定义函数模板或方法 template
你不能用不完整的类型做什么:
使用它的一个实例作为基类 class Foo : X
使用它的一个实例来声明一个成员:class Foo { X
使用其实例之一定义函数或方法 void f1(X
使用其实例化之一的方法或字段,实际上试图取消引用类型不完整的变量 class Foo { X
创建类模板模板 struct X
X
和 X<int>
的语义完全相同,只有前向声明的语法在任何实质性方面有所不同,除了 1 行之外的所有答案都等于只取 Luc 和 {3 }?这真的需要吗?还是我错过了一个不同的小细节?这是可能的,但我已经在视觉上比较了几次,看不到任何...
在您仅使用指向类的指针或引用的文件中。并且不应调用那些指针/引用的成员/成员函数。
with class Foo;
//前向声明
我们可以声明 Foo* 或 Foo& 类型的数据成员。
我们可以用 Foo 类型的参数和/或返回值声明(但不能定义)函数。
我们可以声明 Foo 类型的静态数据成员。这是因为静态数据成员是在类定义之外定义的。
我将其作为单独的答案而不仅仅是评论,因为我不同意 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 的替代方法,并且可以是引入模拟或其他变体的有用方法。重要的区别是一些模板技术,您明确不希望实例化它们,只是为了避免有人对我发脾气。
I disagree with Luc Touraille's answer
所以给他写一条评论,如果你需要篇幅的话,还包括博客文章的链接。这没有回答所提出的问题。如果每个人都认为关于 X 是如何工作的问题有合理的答案,不同意 X 这样做,或者争论我们应该限制我们使用 X 的自由的限制——我们几乎没有真正的答案。
我遵循的一般规则是不包含任何头文件,除非我必须这样做。因此,除非我将类的对象存储为我的类的成员变量,否则我不会包含它,我只会使用前向声明。
只要您不需要定义(想想指针和引用),您就可以摆脱前向声明。这就是为什么大多数情况下您会在标头中看到它们,而实现文件通常会为适当的定义提取标头。
当您想使用其他类型(类)作为类的成员时,通常需要在类头文件中使用前向声明。您不能在头文件中使用前向声明的类方法,因为此时 C++ 还不知道该类的定义。这是您必须移入 .cpp 文件的逻辑,但如果您使用的是模板函数,则应将它们缩减为仅使用模板的部分并将该函数移至标题中。
假设前向声明将使您的代码编译(创建obj)。但是,除非找到定义,否则链接(exe 创建)将不会成功。
class A; class B { A a; }; int main(){}
,让我知道这是怎么回事。当然不会编译。这里所有正确的答案都解释了前向声明 有效的原因和精确的、有限的上下文。相反,你写的是完全不同的东西。
我只想添加一件重要的事情,您可以使用 Luc Touraille 的答案中未提及的转发类来做。
你可以用不完整的类型做什么:
定义接受/返回指向不完整类型的指针/引用并将指针/引用转发给另一个函数的函数或方法。
void f6(X*) {}
void f7(X&) {}
void f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }
一个模块可以将一个前向声明类的对象传递给另一个模块。
正如,Luc Touraille 已经很好地解释了在哪里使用和不使用类的前向声明。
我将补充一下为什么我们需要使用它。
我们应该尽可能使用前向声明来避免不必要的依赖注入。
由于 #include
头文件被添加到多个文件中,因此如果我们将头文件添加到另一个头文件中,它将在源代码的各个部分添加不需要的依赖注入,这可以通过将 #include
头文件添加到 .cpp
文件的任何位置来避免可能而不是添加到另一个头文件并在头 .h
文件中尽可能使用类前向声明。
不定期副业成功案例分享