2012-03-28 30 views
57

只要类声明只使用另一个类作为指针,使用类前向声明​​而不是包含头文件是否有意义,以便先发制人地避免循环依赖关系的问题?所以,而不是:是否应该使用前向声明而不是尽可能包含?

//file C.h 
#include "A.h" 
#include "B.h" 

class C{ 
    A* a; 
    B b; 
    ... 
}; 

做到这一点,而不是:

//file C.h 
#include "B.h" 

class A; 

class C{ 
    A* a; 
    B b; 
    ... 
}; 


//file C.cpp 
#include "C.h" 
#include "A.h" 
... 

是否有任何理由,为什么不尽可能地做到这一点?

+0

简单的答案,没有。 – Nim 2012-03-28 11:20:36

+3

呃 - 这个问题的答案是顶部还是底部? – Mat 2012-03-28 11:21:42

+1

你真正的问题(底部) - AFAIK没有理由不在这种情况下使用前向声明... – Nim 2012-03-28 11:22:40

回答

47

前向声明方法几乎总是更好。 (我不能想到这样一种情况,即包含一个可以使用前向声明的文件更好,但我不会说它总是更好,以防万一)。

有没有缺点,以前瞻性的声明类,但我能想到的一些缺点的用于包括头不必要的:

  • 较长的编译时间,因为所有的翻译单位包括C.h还将包括A.h,虽然他们可能不需要它。

  • 可能包括你不需要间接

  • 污染与符号翻译单元不需要

  • 你可能需要重新编译源文件,其中包括这个头,如果它改变其它头(@PeterWood)

+11

另外,增加了重新编译的机会。 – 2012-03-28 11:40:08

+8

“我无法想象包括可以使用前向声明的文件更好的情况” - 当前向声明产生UB时,请参阅我对主要问题的评论。你是对的谨慎,我认为:-) – 2012-03-28 11:59:52

+0

@SteveJessop我不知道你能做到这一点。但它确实产生了警告。你为什么不加这个作为答案? – 2012-03-28 12:02:25

28

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

一些他们提供的优点是:

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

然而,正向声明一个类可以是特定的类不完全类型和严厉,限制什么样的操作,你可以在不完整的类型进行。
你不能执行任何需要编译器知道类的布局的操作。

一种不完全型可以:

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

一种不完全类型不能:

  • 使用它作为一个基类。
  • 用它来声明一个成员。
  • 定义使用此类型的函数或方法。
+1

“但是,Forward声明一个类会使该特定类为”不完整“类型,并且严重限制您可以在”未完成“类型上执行的操作。”那么是的,但如果你*可以*向前宣布它,这意味着你不需要在标题中的完整类型。如果您确实需要包含该标题的文件中的完整类型,则只需包含所需类型的标题即可。国际海事组织,这是一个优势 - 它迫使你在你的实现文件中包含任何你需要的东西,而不要依赖它被包含在别的地方。 – 2012-03-29 19:47:48

+0

说某人改变了这个标题,并用一个前向声明替换了包含。然后,你必须去改变包含该头的所有文件,使用缺少的类型,但不要自己包含缺失类型的头(尽管它们应该)。 – 2012-03-29 19:48:52

+1

@LuchianGrigore:* ..但是,如果你可以转发声明它...... *,你将不得不尝试检查它。所以没有固定的规则去转发声明和不包含标题,知道规则帮助组织你的实现。Forward声明最常见的用途是打破循环依赖关系,这就是你不能使用Incomplete类型做什么*通常会咬你。每个源文件和头文件都应该包含编译所需的所有头文件,所以第二个参数不适用,它只是一个组织严密的代码开始。 – 2012-03-30 02:47:26

1

是否有任何理由不尽可能地做到这一点?

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

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

+6

命名所有变量'a','b',....,'a1','a2'也可以节省打字。 – 2012-03-28 11:40:29

+0

@Luchian Grigore:对于一些简单的测试程序可能没关系 – ks1322 2012-03-28 11:44:12

14

是否有任何理由不尽可能地做到这一点?

方便。

如果您事先知道这个头文件的任何用户需要包含A的定义来做任何事情(或者大部分时间)。那么只需一次性包含它就很方便。

这是一个相当棘手的问题,因为过于自由的使用这种经验法则会产生一个近乎不可编译的代码。请注意,Boost通过提供特定的“便利”头文件将不同的问题集中到一起,从而以不同的方式解决问题。

+3

这是唯一的答案,指出它有这样的生产力成本。 +1 – usr 2012-05-18 13:48:12

+0

从用户的角度来看。如果您转发声明所有内容,这意味着用户不能只包含该文件并立即开始工作。他们必须弄清楚依赖关系是什么(可能是因为编译器抱怨不完整的类型),并且在开始使用你的类之前也包含这些文件。另一种方法是为你的库创建一个“shared.hpp”文件,其中所有头文件都在该文件中(如上面提到的boost)。他们可以很容易地将其纳入考虑范围,而不必弄清楚为什么他们不能“包含和去”。 – Todd 2017-07-11 14:04:36

8

你不想有前向声明的一种情况是当他们自己很棘手时。

// 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>*); 

预测性声明是一样的重复代码:如果您的某些类的模板,如下面的例子可能发生这种情况,如果代码往往发生很大的变化,你必须改变它在2每次放置或更多,这是不好的。

+2

+1破坏了前向声明总是更好的一致意见:-) IIRC同样的问题发生在通过类型定义“秘密”模板实例化的类型中。 'namespace std {class string; }'即使允许将类声明放在名称空间std中也是错误的,因为(我认为)你不能合法地将类型定义声明为一个类。 – 2012-03-29 11:53:15

0

是否有任何理由不尽可能地做到这一点?

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

在我的电脑的更快()函数运行约2000X比较慢()函数更快:

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(); 
    } 
} 

在其中是关键性能或硬件工作时的应用部位是特别容易为了缓存一致性问题,数据布局和使用可以产生巨大的差异。

这是关于这个问题和其他性能因素好介绍: http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf

+9

您正在回答一个不同的问题(“我应该使用指针吗?”),而不是被问到的问题(“当我仅使用指针时,是否有任何理由不使用前向声明?”)。 – 2014-06-24 15:17:56

6

如果一个使用前向声明,而不是包括在可能的情况?

不,不应将明确的前向声明视为一般准则。前向声明本质上是复制和粘贴的,或者拼写错误的代码,如果你发现它的错误,需要在任何地方使用前向声明来修复。这可能容易出错。

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

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

下面是关于前向声明(无形风险=声明不匹配不是由编译器或连接器检测)“看不见的风险”的一些示例:表示数据可能是不安全的符号

  • 显式前向声明,因为这些前向声明可能需要正确了解数据类型的覆盖区(大小)。

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

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

文件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 a.c b.c 

注:隐形危险,因为G ++没有给出编译器或链接错误/警告
注:省略extern "C"导致对function()一个链接错误由于C++名字改编。

运行程序:

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

是否有任何理由不尽可能地做到这一点?

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

正向声明一个函数:

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

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

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

  • 可能会失去内联优化。

如果使用代码依赖于头,那么所有这些实现细节都可以被函数提供者改变而不会破坏你的代码。

转发声明类:

  • 需要知道它是否是一个派生类和它的衍生基类(ES),

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

  • 要求知道t的真实名称和命名空间因为它可能是一个using声明,将其拉到另一个名称空间,也许在一个别名下,并且需要知道正确的属性(可能它有特殊的对齐要求)。

同样,forward声明中断了这些实现细节的封装,使您的代码更加脆弱。

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

另一种选择是使用pimpl惯用法,它可以更好地隐藏实现细节,并以小的运行时间开销为代价加快编译速度。

相关问题