是否应该使用前向声明而不是尽可能包含?
每当类声明只使用另一个类作为指针时,使用类前向声明而不是包含头文件是否有意义,以便先发制人地避免循环依赖问题? 所以,而不是有:
//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