是否应该使用前向声明而不是尽可能包含?

每当类声明只使用另一个类作为指针时,使用类前向声明​​而不是包含头文件是否有意义,以便先发制人地避免循环依赖问题? 所以,而不是有:

//file Ch #include "Ah" #include "Bh" class C{ A* a; B b; ... }; 

做这个,而不是:

 //file Ch #include "Bh" class A; class C{ A* a; B b; ... }; //file C.cpp #include "Ch" #include "Ah" ... 

有没有任何理由为什么不尽可能做到这一点?

前瞻性声明方法几乎总是比较好的。 (我不能想到包括一个文件,你可以使用前向声明更好的情况,但我不会说这总是更好,以防万一)。

前向声明类没有缺点,但是我可以想到一些不必要的包含头的缺点:

  • 编译时间更长,因为包括Ch在内的所有翻译单元也将包括Ah ,尽pipe他们可能不需要它。

  • 可能还包括其他你不需要间接的头文件

  • 污染翻译单位与您不需要的符号

  • 您可能需要重新编译包含该标头的源文件(@PeterWood)

是的,使用转发声明总是更好。

他们提供的一些优点是:

  • 缩短编译时间。
  • 没有命名空间污染。
  • (在某些情况下)可能会减less生成的二进制文件的大小。
  • 重新编译时间可以显着减less。
  • 避免预处理器名称的潜在冲突。
  • 实现PIMPL Idiom从而提供了一种从接口隐藏实现的方法。

但是,Forward声明一个类使得这个特定的类是一个Incompletetypes ,严重地限制了你可以在Incompletetypes上执行的操作。
你不能执行任何需要编译器知道类的布局的操作。

使用“不完整”types,您可以:

  • 声明一个成员是一个指针或对不完整types的引用。
  • 声明接受/返回不完整types的函数或方法。
  • 定义接受/返回不完整types的指针/引用(但不使用其成员)的函数或方法。

不完整的types,你不能:

  • 用它作为基类。
  • 用它来声明一个成员。
  • 定义使用这种types的函数或方法。

有没有任何理由为什么不尽可能做到这一点?

方便。

如果你提前知道这个头文件的任何用户将需要包含A的定义来做任何事情(或者大部分时间)。 那么把它一劳永逸是方便的。

这是一个相当敏感的话题,因为过于自由地使用这个经验法则会产生一个不可编译的代码。 请注意,Boost通过提供特定的“方便”头文件将不同的问题集中在一起,从而将问题集中在一起。

其中一个你不想有前向声明的例子是当他们自己很棘手的时候。 如果某些类是模板化的,就会发生这种情况,如下例所示:

 // Forward declarations template <typename A> class Frobnicator; template <typename A, typename B, typename C = Frobnicator<A> > class Gibberer; // Alternative: more clear to the reader; more stable code #include "Gibberer.h" // Declare a function that does something with a pointer int do_stuff(Gibberer<int, float>*); 

前向声明和代码重复是一样的:如果代码有很大的变化,那么每次都要修改两个或更多的代码,这是不对的。

是否应该使用前向声明而不是尽可能包含?

不,明确的前瞻性声明不应被视为一般指导原则。 前向声明本质上是复制和粘贴,或拼写错误的代码,如果你发现一个错误,需要在任何地方固定使用前向声明。 这可能是容易出错的。

为避免“向前”声明与其定义之间的不匹配,将声明放在头文件中,并将该头文件包含在定义和声明使用源文件中。

然而,在这种特殊情况下,只有一个不透明的类是前向声明的,这个前向声明可能可以使用,但是一般来说,“尽可能使用前向声明而不是包含”,就像这个线程的标题所说的那样相当危险。

以下是关于前向声明(隐形风险=编译器或链接器未检测到的声明不匹配)的“隐形风险”的一些示例:

  • 表示数据的符号的明确的前向声明可能是不安全的,因为这样的前向声明可能需要对数据types的尺寸(尺寸)的正确认识。

  • 表示函数的符号的显式前向声明也可能是不安全的,如参数types和参数数量。

下面的例子说明了这一点,例如,两个危险的数据前向声明以及一个函数:

文件ac:

 #include <iostream> char data[128][1024]; extern "C" void function(short truncated, const char* forgotten) { std::cout << "truncated=" << std::hex << truncated << ", forgotten=\"" << forgotten << "\"\n"; } 

文件bc:

 #include <iostream> extern char data[1280][1024]; // 1st dimension one decade too large extern "C" void function(int tooLarge); // Wrong 1st type, omitted 2nd param int main() { function(0x1234abcd); // In worst case: - No crash! std::cout << "accessing data[1270][1023]\n"; return (int) data[1270][1023]; // In best case: - Boom !!!! } 

使用g ++ 4.7.1编译程序:

 > g++ -Wall -pedantic -ansi ac bc 

注意:不可见的危险,因为g ++不提供编译器或链接器错误/警告
注意:省略extern "C"会导致function()的链接错误,这是由于c ++名称变形。

运行程序:

 > ./a.out truncated=abcd, forgotten="♀♥♂☺☻" accessing data[1270][1023] Segmentation fault 

有趣的是, 在C ++风格指南中 ,Google推荐使用#include ,而不是为了避免循环依赖。

有没有任何理由为什么不尽可能做到这一点?

绝对:它通过要求类或函数的用户知道和重复实现细节来打破封装。 如果这些实现细节发生变化,则依赖于头部的代码将继续工作,因此可能会破坏转发声明的代码。

正向声明一个函数:

  • 需要知道它是作为一个函数实现的,而不是一个静态函子对象的实例或(gasp!)macros,

  • 要求复制默认参数的默认值,

  • 需要知道它的实际名称和命名空间,因为它可能只是一个using声明,把它拉到另一个名字空间,也许在别名下,

  • 可能会失去在线优化。

如果消费代码依赖于头部,那么函数提供者可以更改所有这些实现细节,而不会破坏您的代码。

向前宣布一个类:

  • 需要知道它是否是派生类以及派生类的基类,

  • 需要知道它是一个类,而不仅仅是一个类模板或一个类模板的特定实例化(或知道它是一个类模板,并获得所有模板参数和默认值正确),

  • 需要知道类的真实姓名和名称空间,因为它可能是一个using声明,将其拉入另一个名称空间,也许在一个别名下,

  • 需要知道正确的属性(也许它有特殊的alignment要求)。

同样,前向声明打破了这些实现细节的封装,使您的代码更加脆弱。

如果你需要削减头文件的依赖来加快编译时间,那么得到类/函数/库的提供者来提供一个特殊的前向声明头文件。 标准库使用<iosfwd>执行此<iosfwd> 。 这个模型保留了实现细节的封装,给了库维护者更改这些实现细节而不破坏代码的能力,同时减less了编译器的负担。

另一个select是使用一个pimpl习语,它可以更好地隐藏实现细节,并以一个很小的运行时间开销为代价加快编译速度。

有没有任何理由为什么不尽可能做到这一点?

我想到的唯一的原因是保存一些打字。

如果没有前向声明,你可以只包含头文件一次,但我不build议在任何相当大的项目上这样做,因为他人指出的缺点。

有没有任何理由为什么不尽可能做到这一点?

是 – 性能。 类对象与其数据成员一起存储在内存中。 当你使用指针时,指向实际对象的内存被存储在堆的其他地方,通常很远。 这意味着访问该对象将导致caching未命中并重新加载。 这在性能至关重要的情况下可能会有很大的不同。

在我的PC上,Faster()函数的运行速度比Slower()函数大约快2000倍:

 class SomeClass { public: void DoSomething() { val++; } private: int val; }; class UsesPointers { public: UsesPointers() {a = new SomeClass;} ~UsesPointers() {delete a; a = 0;} SomeClass * a; }; class NonPointers { public: SomeClass a; }; #define ARRAY_SIZE 100000 void Slower() { UsesPointers list[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { list[i].a->DoSomething(); } } void Faster() { NonPointers list[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { list[i].a.DoSomething(); } } 

在对性能要求严格的应用程序或者在硬件上工作的部分应用程序中,特别容易出现caching一致性问题时,数据布局和使用情况可能会产生巨大的差异。

这是一个很好的主题和其他performance因素: http : //research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf