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

时间:2012-03-28 11:19:18

标签: c++ forward-declaration

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

//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"
...

为什么不尽可能不这样做?

9 个答案:

答案 0 :(得分:55)

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

前向声明类没有任何缺点,但我可以想到不必要地包含标题的一些缺点:

  • 编译时间较长,因为包括C.h在内的所有翻译单元也会包含A.h,但他们可能不需要它。

  • 可能包括您不需要间接需要的其他标题

  • 使用您不需要的符号污染翻译单元

  • 如果源文件发生变化,您可能需要重新编译包含该标题的源文件(@PeterWood)

答案 1 :(得分:35)

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

他们提供的一些优势是:

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

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

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

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

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

  • 将其用作基类。
  • 用它来宣布成员。
  • 使用此类型定义函数或方法。

答案 2 :(得分:18)

  

为什么不尽可能不这样做?

便利。

如果你事先知道这个头文件的任何用户都必须要包含A的定义来做任何事情(或者大多数时候)。然后一次性包含它就很方便。

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

答案 3 :(得分:10)

您不希望拥有前向声明的一种情况是它们本身很棘手。如果您的某些类是模板化的,则会发生这种情况,如下例所示:

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

答案 4 :(得分:7)

  

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

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

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

然而,在这种特殊情况下,只有前向声明的前向声明,这个前向声明可以使用,但一般情况下,“使用前向声明而不是包含尽可能包括”,就像这个线程的标题一样说,风险很大。

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

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

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

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

档案a.c:

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

文件b.c:

#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 ++没有给出编译器或链接器错误/警告 注意:由于c ++名称损坏,省略extern "C"会导致function()的链接错误。

运行程序:

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

答案 5 :(得分:6)

有趣的事实,in its C++ styleguide,Google建议在任何地方使用#include,但要避免循环依赖。

答案 6 :(得分:4)

  

为什么不尽可能不这样做?

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

转发声明功能:

  • 要求知道它是作为函数实现的,而不是静态函子对象的实例或(喘气!)宏,

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

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

  • 可能会失去内联优化。

如果消费代码依赖于标题,则函数提供程序可以更改所有这些实现细节,而不会破坏您的代码。

转发声明课程:

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

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

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

  • 需要知道正确的属性(可能它有特殊的对齐要求)。

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

如果您需要剪切标头依赖项以加快编译时间,那么请获取类/函数/库的提供程序以提供特殊的前向声明标头。标准库使用<iosfwd>执行此操作。该模型保留了实现细节的封装,并使库维护者能够在不破坏代码的情况下更改这些实现细节,同时减少编译器的负担。

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

答案 7 :(得分:2)

  

为什么不尽可能不这样做?

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

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

答案 8 :(得分:-1)

  

为什么不尽可能不这样做?

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

在我的电脑上,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();
    }
}

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