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

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)

  • 此外,增加了重新编译的机会. (11认同)
  • "我想不出包含一个你可以使用前向声明的文件更好的情况" - 当前向声明产生UB时,请参阅我对主要问题的评论.我认为你是谨慎的:-) (9认同)
  • 缺点是更多的工作和更多的代码!更脆弱.你不可能说没有缺点. (2认同)
  • 如果有5个课程怎么办?如果您以后需要添加一些内容怎么办?你只关注你的观点的最佳案例. (2认同)
  • 我可以想到一些可能会出现问题的情况:向前声明可能以“struct”开始然后更改为“class”的类型,这可能会在某些静态分析器中触发警告。前向声明使用诸如“typedef std::vector<MyType>;”之类的模板的类型,如果稍后要更改模板类型,则需要更改所有前向声明。 (2认同)

Alo*_*ave 37

是的,使用前向声明总是更好.

它们提供的一些优点是:

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

但是,Forward声明一个类会使该特定类成为不完整类型,并严重限制您可以对不完整类型执行的操作.
您无法执行任何需要编译器知道类布局的操作.

使用不完整类型,您可以:

  • 将成员声明为指针或对不完整类型的引用.
  • 声明接受/返回不完整类型的函数或方法.
  • 定义接受/返回指向不完整类型的指针/引用的函数或方法(但不使用其成员).

对于不完整类型,您不能:

  • 将它用作基类.
  • 用它来声明一个成员.
  • 使用此类型定义函数或方法.


Mat*_* M. 18

有什么理由不尽可能不这样做吗?

方便.

如果你提前知道这个头文件的任何用户都必须包含A做任何事情的定义(或者大多数时候).然后,只需一劳永逸地包含它就很方便.

这是一个相当棘手的主题,因为过于自由地使用这种经验法则会产生一个近乎无法编译的代码.请注意,Boost通过提供特定的"便利"标题来解决问题,这些标题将几个紧密的功能捆绑在一起.

  • 这是唯一的答案,指出它有这样做的生产力成本.+1 (4认同)

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个或更多位置进行更改,这并不好.

  • +1用于破坏前向声明严格总是更好的共识:-) IIRC同样的问题出现在类型是"秘密"模板实例化,通过typedef.`namespace std {class string; 即使允许将类声明放在命名空间std中也是错误的,因为(我认为)你不能合法地向前声明一个typedef,就像它是一个类一样. (2认同)

Blu*_*mon 8

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

不,明确的前瞻性声明不应被视为一般准则.前向声明本质上是复制和粘贴,或拼写错误的代码,如果您发现其中的错误,需要在使用前向声明的任何地方修复.这可能容易出错.

为了避免"转发"声明与其定义之间的不匹配,将声明放在头文件中,并在定义和声明使用源文件中包含该头文件.

然而,在这种特殊情况下,只有前向声明的前向声明,这个前向声明可以正常使用,但一般来说,"使用前向声明而不是尽可能包括",就像这个主题的标题所说的那样,风险很大.

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

  • 表示数据的符号的显式前向声明可能是不安全的,因为这种前向声明可能需要正确了解数据类型的覆盖区(大小).

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

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

文件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)


Zou*_*uch 8

有趣的是,在其C++风格指南中,Google建议使用#include无处不在,但要避免循环依赖.


Adr*_*thy 5

有什么理由不这样做吗?

绝对:通过要求类或函数的用户知道并复制实现细节来破坏封装。如果那些实现细节发生变化,则依赖于头文件的代码将继续工作,而前向声明的代码可能会被破坏。

转发声明函数:

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

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

  • 需要知道其实际名称和名称空间,因为它可能只是将using其拉入另一个名称空间(可能是在别名下)的声明,并且

  • 可能会失去在线优化功能。

如果使用方代码依赖于标头,则功能提供者可以更改所有这些实现细节,而不会破坏您的代码。

转发声明一个类:

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

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

  • 需要知道该类的真实名称和名称空间,因为它可能是一个using声明,可以将其拉入另一个名称空间(可能是在别名下),并且

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

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

如果需要减少头文件的依赖关系以加快编译时间,请获取类/函数/库的提供者以提供特殊的前向声明头文件。标准库使用<iosfwd>。该模型保留了实现细节的封装,并使库维护者可以在不破坏代码的情况下更改这些实现细节,同时减少编译器的负担。

另一个选择是使用pimpl惯用语,它更好地隐藏了实现细节,并以少量运行时开销为代价来加快编译速度。