C++中前向声明比include好吗?
声明和定义
声明(declartion)指定了一个实体唯一的名字,以及其类型和其它特征的信息。定义(definition)为编译期提供了它去为被使用的实体生成机器码的一切信息。
所有的实体在被使用前都需要被声明。
声明和定义并不是完全隔离的,下面的代码里面每一个实体都既是声明又是定义
int i;
int j = 10;
enum suits { Spades = 1, Clubs, Hearts, Diamonds };
class CheckBox : public Control
{
public:
Boolean IsChecked();
virtual int ChangeState() = 0;
};
下面的代码就只是声明而非定义
extern int i;
char *strchr( const char *Str, const char Target );
class A;
我们一般都把声明(或者声明兼定义)放在头文件里面,把实现放在.cpp文件里面。如果要在一个头文件里面使用另外一个头文件里面的声明或者定义,一般有两种方式:
- 前向声明;
- include另外一个头文件。
这两者是否有差别呢?什么时候使用前向声明更加合适,什么时候使用include更加合适?
使用前向声明解决循环依赖
我们的代码里面会经常出现循环依赖的情况,比如
class A
{
private:
B *ptr;
};
class B
{
public:
A* getA() {...}
};
这时候,我们会发现,如果把class A
放在A.h
;class B
放在B.h
。那无论是A.h
去includeB.h
还是B.h
去include A.h
都不合适。如果把它们都放到同一个头文件里面,也存在定义A
的时候需要可见B
;定义B
的时候需要可见A
的问题,把谁写在前面都不合适的问题。
要解决这个问题,我们只能使用前向声明(forward declartion):
// forward declare class B
class B;
class A
{
private:
B *ptr;
};
class B
{
public:
A* getA() {...}
};
使用前向声明解决隐藏实现
我们在封装C++编写的代码的对外接口的时候会经常使用PIMPL模式来隐藏实现。
// interface.h
#include <memory>
class Impl;
class Interface final
{
public:
Interface();
~Interface();
bool prepare();
bool run();
bool refill();
private:
std::unique_ptr<Impl> m_impl;
}
真实的实现都在Impl里面,但是我们并不想把实现细节暴露给使用者,所以在interface.h
里面我们是不能#include "Impl.h"
的,只能使用前向声明来让Impl
对编译器可见。
这里需要注意的是我们一定需要定义custom的析构函数,连~Interface() = default
都不行。大家可以思考一下问什么,答案在稍后的讨论中揭晓。
使用前向声明提升编译性能
C++的include并没有什么玄妙,只是copy-paste的操作。但是这样的操作对编译影响很大。
假设有下面的代码
// A.h
class A
{
};
// 此处省略一万行
// B.h
#include "A.h"
class B
{
private:
A *a;
};
// 此处省略一万行
// C.h
#include "B.h"
class C
{
public:
//...
private:
B *b;
};
//...
那么C.h
里面除了本身的内容外,还有两万多行从A.h, B.h
里面来的内容。这会极大增加编译器处理时间。
另外一方面,当class A
的定义有任何改动的时候,B.cpp
和C.cpp
都需要重新编译,因为它们文件里面的内容因为A.h
的改变都改变了。这也将极大增加编译器处理时间。
相反,如果我们把上面的代码修改为
// A.h
class A
{
};
// 此处省略一万行
// B.h
class A;
class B
{
private:
A *a;
};
// 此处省略一万行
// C.h
class B;
class C
{
public:
//...
private:
B *b;
};
//...
那我们就代码进行了解耦合,编译C.cpp
的时候不再需要处理额外的两万行代码;class A
进行了变动的时候编译器也能进行最小的改动即可达到适配改动,而不用A.cpp, B.cpp, C.cpp
完全重新进行编译,这极大提升了编译效率。
什么时候可以使用前向声明?
这个问题可以换一个问法:
什么时候一定需要定义?
在编译器需要需要去决定它要实例化的一个对象的大小以及内存布局的时候,编译器一定需要定义而非仅仅声明。
其它情况下,都可以使用前向声明。
参考文献[1]中使用了一段很形象的代码来举例
#include "BaseClass.h"
#include "Member.h"
#include "AnotherType.h"
class Pointee;
class ReturnType;
class ArgumentType;
class MyClass : public BaseClass {
Member aMember; //definition needed
Pointee* aPointer; //declaration is enough
public:
ReturnType funcDecl(ArgumentType arg);
Pointee* ptrFuncDef(ArgumentType const& ref) {
//function definition, ArgumentType
//is only use by reference, no defintion needed
//same for Pointee
return aPointer;
}
AnotherType anotherFunc(AnotherType other) {
//AnotherType is copied, so the definition is needed
return other;
}
};
里面需要单独说明的是函数在声明的时候,其参数和返回值类型都只需要前向声明;在定义的时候就需要视情况而定,大多数情况下需要定义而非单单声明。
到这里,我们就可以回答为什么PIMPL里面一定需要自定义析构函数:
如果我们不自定义析构函数或者使用~Interface()=default
这样的写法,编译器就会在头文件为我们的类生成析构函数。而生成的析构函数需要析构unique_ptr
的实例,unique_ptr
的析构函数需要知道具体的类型定义才能知道其析构的顺序(析构的时候需要先析构子类,再析构父类),这就需要Impl
的定义而非仅仅是前向声明。如果我们自定义了析构函数,就可以在cpp文件里面包含Impl的定义文件以满足上面的需求。
枚举
很奇怪的是:老式的枚举不支持前向声明,enum class
以及带有底层类型的枚举却可以:
enum OldEnum; //ERROR
enum WithUnderlyingType : short; //OK
enum class Scoped; //OK
enum class ScopedWithType : int; //OK
内联函数
内联函数本来就是想让编译期直接以实现代替函数调用,使用内联函数的时候自然是需要见到内联函数的定义的。在这种情况下,我们只能使用include的模式而非前向声明内联函数的模式。
系统库头文件以及模版
前面说了一堆使用前向什么的好处,是不是都使用前向什么更好呢?
其实不是的。
比如std::string
,我们如果只是在使用的时候使用前向声明,编译一样会报错。因为它其实只是basic_string<char>
的typedef而已。
而对于其它的一些模版其实也一样,头文件里面有大量的实现细节,最终给外部使用的类型大量依赖这些实现细节。我们不可能完全了解这些细节,并且即便了解了也只能假设不了解,因为这些实现细节随时可能会变。在这样的情况下,系统库函数以及模版类型头文件我们最好直接include而非使用前置声明。
结语
前置什么不光是可以提升性能,还是在解决循环依赖问题的时候的必要武器。但是并非可以一以概之地使用它,而是需要具体问题具体分析。
参考文献
- Forward Declarations to Reduce Compiletime Dependencies: https://arne-mertz.de/2018/03/forward-declarations/
欢迎关注公众号,关注知乎号,一起提升技术!