前置声明

定义

所谓前置声明(forward declaration)是类, 函数和模板的纯粹声明, 没伴随着其定义.

使用场景

头文件A包含另一个头文件B, 是为了引入在头文件A中使用的类, 函数, 结构体, 枚举或其他实体的声明. 一般来说, 只有在自己的类中将某个类的对象作为数据成员使用时, 或者需要继承某个类时, 才应该包含那个类的头文件. 传统上来说, 前置声明可以在下列情况下使用:

  1. 不需要知道类的大小. 如果包含的类要作为成员变量或打算从包含类派生子类, 那么编译器需要知道类的大小.

  2. 没有引用类的任何成员方法. 引用类的成员方法需要知道方法原型, 即参数和返回值类型.

  3. 没有引用类的任何成员变量. 不过, 本身就不应该把类的成员变量暴露.

    比如:

    1
    2
    3
    4
    5
    6
    7
    8
    class B;

    class A {
    public:
    void SetObject(const B& obj);
    private:
    B* m_obj;
    };

    如果要修改A的定义使得编译器知道类B的实际大小, 那么必须包含类B实际的声明, 即必须包含B的头文件:

    1
    2
    3
    4
    5
    6
    7
    8
    #include <b.h>

    class A {
    public:
    void SetObject(const B& obj);
    private:
    B m_obj;
    }

优点

  1. 前置声明能够节省编译时间, 多余的#include会迫使编译器展开更多的文件, 处理更多的输入.

  2. 前置声明能够节省不必要的重新编译时间. #include使代码因为头文件中无关的改动而被重新编译多次.

缺点

  1. 前置声明隐藏关系, 头文件改动时, 用户代码会跳过必要的重新编译过程.

  2. 前置声明可能会被库的后续更改所破坏. 前置声明函数或模板有时会妨碍头文件变动其API. 例如扩大参数类型, 加上自带默认参数的模板形参等.

  3. 前置声明来自命名空间std::的symbol时, 其行为未定义.

  4. 前置声明可能会破坏逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // b.h
    struct B {};
    struct D : B {};

    // good_user.cc

    #include "b.h"
    // struct B;
    // struct D;

    void f(B*) {

    }

    void f(void*) {

    }

    int main() {
    f(nullptr);
    }

    如果是使用前置声明代替#include, 那么实际上会调用f(void*), 因为前置声明会隐藏类的依赖关系.

  5. 前置声明了不少来自头文件的symbol时, 就会比单单一行的include冗长.

  6. 仅仅为了能前置声明而重构代码(比如使用指针成员代替对象成员)会使代码变得更慢更复杂.

  7. delete一个不完整类型的指针时, 如果这个类型有non-trival的析构函数, 那么这种行为是未定义的.

结论

  1. 尽量避免前置声明那些定义在其他项目中的实体.

  2. 函数: 总是使用#include.

  3. 类模板: 优先使用#include.