Mat*_*Mat 74 c++ forward-declaration
每当类声明仅使用另一个类作为指针时,使用类前向声明而不是包含头文件是否有意义,以便先发制人地避免循环依赖的问题?所以,而不是:
//file C.h
#include "A.h"
#include "B.h"
class C{
A* a;
B b;
...
};
Run Code Online (Sandbox Code Playgroud)
改为:
//file C.h
#include "B.h"
class A;
class C{
A* a;
B b;
...
};
//file C.cpp
#include "C.h"
#include "A.h"
...
Run Code Online (Sandbox Code Playgroud)
有什么理由不尽可能不这样做吗?
Luc*_*ore 58
前向声明方法几乎总是更好.(我想不出包含一个你可以使用前向声明的文件更好的情况,但我不会说它总是更好,以防万一).
前向声明类没有任何缺点,但我可以想到不必要地包含标题的一些缺点:
编译时间较长,因为所有翻译单元C.h也包括在内A.h,尽管他们可能不需要它.
可能包括你不需要间接的其他标题
使用您不需要的符号污染翻译单元
您可能需要重新编译包含该标头的源文件(如果它发生更改)(@ PeterWood)
Alo*_*ave 37
是的,使用前向声明总是更好.
它们提供的一些优点是:
但是,Forward声明一个类会使该特定类成为不完整类型,并严重限制您可以对不完整类型执行的操作.
您无法执行任何需要编译器知道类布局的操作.
使用不完整类型,您可以:
对于不完整类型,您不能:
Mat*_* M. 18
有什么理由不尽可能不这样做吗?
方便.
如果你提前知道这个头文件的任何用户都必须包含A做任何事情的定义(或者大多数时候).然后,只需一劳永逸地包含它就很方便.
这是一个相当棘手的主题,因为过于自由地使用这种经验法则会产生一个近乎无法编译的代码.请注意,Boost通过提供特定的"便利"标题来解决问题,这些标题将几个紧密的功能捆绑在一起.
ana*_*lyg 11
您不希望拥有前向声明的一种情况是它们本身很棘手.如果您的某些类是模板化的,则会发生这种情况,如下例所示:
// 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>*);
Run Code Online (Sandbox Code Playgroud)
前向声明与代码重复相同:如果代码往往会发生很大的变化,那么每次都必须在2个或更多位置进行更改,这并不好.
是否应该使用前向声明而不是尽可能包括?
不,明确的前瞻性声明不应被视为一般准则.前向声明本质上是复制和粘贴,或拼写错误的代码,如果您发现其中的错误,需要在使用前向声明的任何地方修复.这可能容易出错.
为了避免"转发"声明与其定义之间的不匹配,将声明放在头文件中,并在定义和声明使用源文件中包含该头文件.
然而,在这种特殊情况下,只有前向声明的前向声明,这个前向声明可以正常使用,但一般来说,"使用前向声明而不是尽可能包括",就像这个主题的标题所说的那样,风险很大.
以下是有关前向声明的"隐形风险"的一些示例(隐形风险=编译器或链接器未检测到的声明不匹配):
表示数据的符号的显式前向声明可能是不安全的,因为这种前向声明可能需要正确了解数据类型的覆盖区(大小).
表示函数的符号的显式前向声明也可能是不安全的,如参数类型和参数数量.
下面的例子说明了这一点,例如,两个危险的数据前向声明以及一个函数:
文件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";
}
Run Code Online (Sandbox Code Playgroud)
文件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 !!!!
}
Run Code Online (Sandbox Code Playgroud)
使用g ++ 4.7.1编译程序:
> g++ -Wall -pedantic -ansi a.c b.c
Run Code Online (Sandbox Code Playgroud)
注意:不可见的危险,因为g ++没有给出编译器或链接器错误/警告
注意:由于c ++名称extern "C"错误导致忽略导致链接错误function().
运行程序:
> ./a.out
truncated=abcd, forgotten="?????"
accessing data[1270][1023]
Segmentation fault
Run Code Online (Sandbox Code Playgroud)
有什么理由不这样做吗?
绝对:通过要求类或函数的用户知道并复制实现细节来破坏封装。如果那些实现细节发生变化,则依赖于头文件的代码将继续工作,而前向声明的代码可能会被破坏。
转发声明函数:
需要知道它是作为一个函数实现的,而不是静态函子对象或(gasp!)宏的实例,
需要复制默认参数的默认值,
需要知道其实际名称和名称空间,因为它可能只是将using其拉入另一个名称空间(可能是在别名下)的声明,并且
可能会失去在线优化功能。
如果使用方代码依赖于标头,则功能提供者可以更改所有这些实现细节,而不会破坏您的代码。
转发声明一个类:
需要知道它是否是派生类以及它的派生基类,
需要知道它是一个类,而不仅仅是一个typedef或一个类模板的特定实例(或者知道它是一个类模板,并确保所有模板参数和默认值正确),
需要知道该类的真实名称和名称空间,因为它可能是一个using声明,可以将其拉入另一个名称空间(可能是在别名下),并且
需要知道正确的属性(也许有特殊的对齐要求)。
同样,前向声明破坏了这些实现细节的封装,使您的代码更加脆弱。
如果需要减少头文件的依赖关系以加快编译时间,请获取类/函数/库的提供者以提供特殊的前向声明头文件。标准库使用<iosfwd>。该模型保留了实现细节的封装,并使库维护者可以在不破坏代码的情况下更改这些实现细节,同时减少编译器的负担。
另一个选择是使用pimpl惯用语,它更好地隐藏了实现细节,并以少量运行时开销为代价来加快编译速度。