C++如果都能优先用前向声明,还要include做什么?
今天书上看到一段话,也是一个疑问,我在想如果都能优先前向声明,还要include做什么?书上内容: 如果你觉得你在C++文件中包含了太多内容,请考虑使用名为in...
- 10 个点赞 👍
前向声明是必要的技巧,但用前向声明去代替包含头文件,恰恰是不推荐的方式。
参见:
下面是(非官方)中文版中的部分内容。
缺点:
- 前向声明隐藏了依赖关系, 可能会让人忽略头文件变化后必要的重新编译过程.
- 相比
#include
, 前向声明的存在会让自动化工具难以发现定义该符号的模块. - 修改库 (library) 时可能破坏前向声明. 函数或模板的前向声明会阻碍头文件的负责人修改 API, 例如拓宽 (widening) 参数类型, 为模版参数添加默认值, 迁移到新的命名空间等等, 而这些操作本是无碍的.
- 为
std::
命名空间的符号提供前向声明会产生未定义行为 (undefined behavior). - 很难判断什么时候该用前向声明, 什么时候该用
#include
. 用前向声明代替#include
时, 可能会悄然改变代码的含义:
// b.h: struct B {}; struct D : B {}; // good_user.cc: #include "b.h" void f(B*); void f(void*); void test(D* x) { f(x); } // 调用 f(B*)
若用
B
和D
的前向声明替代#include
,test()
会调用f(void*)
.- 为多个符号添加前向声明比直接
#include
更冗长. - 为兼容前向声明而设计的代码 (比如用指针成员代替对象成员) 更慢更复杂.
结论:
尽量避免为其他项目定义的实体提供前向声明.
发布于 2024-05-04 12:12・IP 属地上海查看全文>>
吴咏炜 - 3 个点赞 👍
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
去includeA.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/
欢迎关注公众号,关注知乎号,一起提升技术!
发布于 2024-05-04 16:43・IP 属地北京查看全文>>
杜凌霄 - 2 个点赞 👍
查看全文>>
肖堂-数蚕 - 2 个点赞 👍
C++编译时间长确实是个问题,但是减少编译时间并不是做具体项目的人优先考虑的问题,除非它真的长到严重影响工作了。现代的C++语言和具体编译器会采用把头文件改成模块、以及预编译头文件等方式来减少编译时间,不要自作聪明采用不规范的方法。
绝大多数情况下,用前向声明替代标准库、系统开发包和特定用途库的头文件都是极不可取的,属于小聪明误大事。因为很多重要的头文件里面包含了非常多的条件编译分支,绝对不可以从头文件里提取出你当前用到的声明放到cpp文件里而扔掉头文件,否则编译配置选项稍微一变化,就要出错了。
还有,C++的头文件和C还不一样,C的头文件里面基本上都是数据类型声明和函数原型,而C++的头文件里面最多的是模板。题主看的书可能是C语言或者C++早期的书,已经过时了。
编辑于 2024-05-06 09:09・IP 属地浙江查看全文>>
望山 - 2 个点赞 👍
C++如果都能优先用前向声明,还要include做什么?
今天书上看到一段话,也是一个疑问,我在想如果都能优先前向声明,还要include做什么?
书上内容: 如果你觉得你在C++文件中包含了太多内容,请考虑使用名为include-what-you-use的工具来整理它们。优先前向声明(forward declaring)类型和函数而不是包括头文件也对减少编译时间有很大帮助。前向声明(forward declaration)和 #include 在 C++ 中各有其用途,它们并不是相互排斥的,而是互为补充的。
前向声明允许你在不实际包含某个头文件的情况下,声明一个类型或函数的存在。这有几个主要的好处:
减少编译时间:由于编译器不需要处理未实际使用的头文件中的所有内容,因此可以减少编译时间。
减少依赖:前向声明可以减少头文件之间的依赖关系,使代码更加模块化。
隐藏实现细节:通过只提供前向声明,你可以隐藏某个类型或函数的实现细节,只暴露必要的接口。
然而,前向声明也有一些限制和缺点:
不完整类型:前向声明的类型是不完整的(incomplete type)。这意味着你不能创建该类型的实例,也不能访问其成员(除非它们是静态的)。因此,前向声明通常只用于指针或引用类型的声明。
需要实际定义:虽然你可以使用前向声明来声明一个类型或函数的存在,但在实际使用它们之前,你仍然需要包含相应的头文件以提供完整的定义。
可能增加复杂性:过度使用前向声明可能会导致代码更加难以理解和维护。如果某个头文件中的类型或函数被频繁地前向声明,那么可能需要跟踪多个地方来查看它们的实际定义。
因此,尽管前向声明在某些情况下可以减少编译时间和依赖关系,但它并不能完全替代 #include。以下是使用 #include 的一些原因:
获取完整的类型定义:当你需要创建某个类型的实例或访问其成员时,你需要包含相应的头文件以获取完整的类型定义。
访问函数声明:如果你需要调用某个函数,你需要包含该函数声明的头文件。虽然你可以通过前向声明来声明函数的存在,但你需要包含头文件来获取函数的实际声明(包括参数类型和返回类型)。
模板和内联函数:模板和内联函数的定义通常需要在每个使用它们的文件中可见。这意味着你需要包含相应的头文件来提供这些定义。
头文件保护和包含守卫:通过使用 #ifndef、#define 和 #endif 指令(或者更现代的 #pragma once),头文件可以防止被多次包含,从而避免重复定义错误。这是前向声明无法提供的。
综上所述,前向声明和 #include 各有其用途和限制。在编写 C++ 代码时,应根据具体情况选择使用哪种方法。在某些情况下,使用前向声明可以减少编译时间和依赖关系;而在其他情况下,使用 #include 可以提供完整的类型定义、函数声明以及模板和内联函数的定义。
发布于 2024-05-04 08:13・IP 属地北京查看全文>>
知乎用户 - 1 个点赞 👍
前向声明在很多情况下能够帮助减少编译时间和解决循环依赖的问题,但它并不能完全替代#include指令。
这是因为前向声明和包含头文件(#include)有各自不同的用途和限制:
1. 前向声明的目的:
• 减少编译依赖:通过仅声明一个类、结构或函数的存在,而不需要知道其完整定义,可以减少编译时的文件依赖关系,加快编译速度。
• 解决循环依赖:当两个或多个类相互引用对方时,前向声明可以打破循环,使得每个类只知道对方的存在而不立即解析其细节。
2. #include的作用:
• 提供完整定义:为了实例化对象、调用函数的具体实现或者继承类等,需要类或函数的完整定义,这时就必须使用#include来包含对应的头文件。
• 访问具体成员:如果代码中需要直接访问某个类的成员变量或具体的成员函数,前向声明是不够的,必须包含完整的定义。
• 编译时检查:包含头文件允许编译器进行更彻底的类型检查,确保代码的正确性。
简而言之,前向声明主要用于告知编译器某个名称的存在,而#include则是为了获取实现这些名称所需的所有详细信息。
虽然推荐优先考虑前向声明以优化编译时间和管理依赖,但在实际编程中,两者是相辅相成的,根据具体情况选择使用。
例如,当你确实需要一个对象实例或访问类的内部细节时,就必须使用#include来包含那个类的完整定义。
发布于 2024-05-06 06:53・IP 属地北京查看全文>>
深耕AI - 1 个点赞 👍
在C++中,前向声明和头文件包含(
#include
)是两种常用的方法,它们在不同的情况下各有其作用和优势。理解它们的区别和适用场景对于编写高效、可维护的C++代码非常重要。前向声明
前向声明允许你在实际定义一个类、结构体或函数之前声明其存在。这样做的主要好处是可以减少编译依赖,因此可能会减少编译时间,并避免一些依赖循环问题。前向声明最适用于以下情况:
- 当你只需要引用一个对象的指针或引用时,可以使用前向声明。例如,如果你的函数或类仅仅需要声明某个类的指针或引用,而不需要访问其具体的成员或方法,那么前向声明就足够用了。
- 在声明类的成员为另一个类的指针或引用时。
头文件包含(
#include
)头文件包含是通过
#include
指令将一个文件的内容插入到另一个文件中。如果你的代码需要知道某个类型的具体结构,或者需要调用某个类型的成员函数,那么就必须包含该类型的完整定义,这通常通过包含相应的头文件来实现。头文件包含是必要的,因为:- 如果你要声明某个类的对象,或者需要继承某个类,那么你需要类的完整定义。
- 如果你的函数需要使用某个类型的实例(而不仅仅是指针或引用),或者需要调用其成员函数,那么也需要类的完整定义。
- 对于模板类或模板函数的实例化,也需要其完整的定义。
结论
尽管前向声明可以减少编译时间和解决依赖问题,但它只能在你不需要访问实体的具体内容时使用。相反,如果需要类型的完整信息,
#include
是不可避免的。因此,虽然在可能的情况下使用前向声明是一个好的实践,但它不能完全取代头文件的包含。理解何时使用前向声明,何时必须使用#include
,是成为一个高效C++开发者的重要部分。发布于 2024-05-06 08:31・IP 属地福建查看全文>>
Javen - 0 个点赞 👍
查看全文>>
旧时代