如何停止通过分层包含传播声明
How to stop propagating declarations through hierarchical includes?
每当我创建.h头文件时,一个问题出现在我的脑海中:"如何停止通过分层包含传播声明?"假设存在以下文件:
foo。
#ifndef FOO_H
#define FOO_H
typedef int foo_t;
inline int foo() { return 1; }
class foo_c {};
#endif /* FOO_H */
bar.h
#ifndef BAR_H
#define BAR_H
#include "Foo.h"
typedef foo_t bar_t;
inline int bar() { return foo(); }
class bar_c : public foo_c {};
#endif /* BAR_H */
zoo.h
#ifndef ZOO_H
#define ZOO_H
#include "Bar.h"
typedef bar_t zoo_t;
inline int zoo() { return bar(); }
class zoo_c : public bar_c {};
#endif /* ZOO_H */
在zoo.h文件中,我们可以访问已声明的元素foo_c
, foo_t
, foo()
,并且每次对foo.h的修改都会重新编译 zoo.h
作为Qt中的一个例子,当我包括和使用<QQueue>
时,我无法访问QList
,其中QQueue
由QList
继承,我必须显式地包括<QList>
。(另外,我不知道它是如何完成的,以及它对编译时间的影响)
在c++和C中,"停止传播声明"需要从公共接口中删除它们,句号。将它们转移到实现中。或者到"less public"接口
编译时间是目标之一。其他是可移植性、可维护性。这也与松耦合直接相关。
最流行的c++技术,可以帮助您的类派生是Pimpl习语。派生实现类,将相应的头文件包含到实现cpp中,并在公共接口中前向声明实现。你的用户将对基类一无所知,他们只知道你的实现的名字。
如果你想使用typedef
,它是不可能停止传播的,但是为了提供更好的可移植性和可维护性,你可以使用与Boost库有效使用的相同方法:实现定义类型(例如这个)。
每个接口设计都是可扩展性、信息隐藏和简单性(或工作量)之间的权衡。如果您需要存档前两个,请使用更复杂的方法。您可以提供两个公共接口:一个用于使用,另一个更广泛、级别更低,用于可扩展性。
我发现在我的代码中清楚地区分前向声明和定义是很重要的:尽可能多地使用前向声明。
一般来说,如果你的类X不需要知道类Y的大小,你所需要的只是Y的前向声明——你不需要包括Y。
例如,如果X不是Y的子类,并且X不包含任何类型Y的成员,则不需要包含Y.hpp。前向声明类Y;就足够了。有时,为了更好地解耦我的代码,我会保留一个Y的引用或指针,而不是将Y嵌入到类X中——如果这是可行的,我所需要做的就是向前声明类Y;现在,有一个关于使用模板类时不能向前声明的注释。但是这里有一个技巧—不是使用typedef,而是使用您想要的模板实例化的子类,例如:
class Bars : public std::vector<Bar> { };
现在可以前向声明class Bars;
,而以前不能前向声明std::vector<Bar>;
所以,这些是我在所有c++项目中遵循的步骤:
- 将我的代码分成按命名空间划分的模块
- 在每个模块中创建一个 fdecle .hpp文件,该文件包含该模块的转发声明
- 强烈建议使用
#include <modulename/fdecl.hpp>
而不是任何#include <modulename/foo.hpp>
(前向声明优于定义)
这样,头是松散耦合的,当我修改代码时,我得到了更快的编译时间。
我将这样重写代码:
foo。
#ifndef FOO_H
#define FOO_H
inline int foo();
#endif /* FOO_H */
foo.cpp
#include "foo.h"
inline int foo()
{
return 1;
}
bar.h
#ifndef BAR_H
#define BAR_H
inline int bar();
#endif /* BAR_H */
bar.cpp
#include "bar.h"
#include "foo.h"
inline int bar()
{
return foo();
}
zoo.h
#ifndef ZOO_H
#define ZOO_H
inline int zoo();
#endif /* ZOO_H */
zoo.cpp
#include "zoo.h"
#include "bar.h"
inline int zoo()
{
// cannot *incidentally* access foo() here, explicit #include "foo.h" needed
return bar();
}
这样你就只在头文件中公开你的接口,而实现细节仍然保存在.cpp文件/.
请注意,如果你使用模板,这个策略将会失败:它们必须在头文件中完全声明(否则你可能会遇到链接器问题)。
也许您可以使用名称空间:
foo。
namespace f {
inline int foo();
}
bar.h
#include "foo.h"
inline int bar()
{
using namespace f;
return foo();
}
zoo.h
#include "bar.h"
inline int zoo()
{
using namespace b;
// Cannot use foo here: can only refer to it by the full name f::foo
return bar();
}
这个例子看起来很做作,但可能只是因为代码太短了。如果您的应用程序涉及更多的代码,这个技巧可能会被证明是有用的。
:
同样的原则也适用于类和其他名称。例如,使用Qt名称:
qt_main.h
namespace some_obscure_name
{
class QList {...};
class QQueue: public QList {...}
...
}
qt_list.h
#include "qt_main.h"
using some_obscure_name::QList;
qt_queue.h
#include "qt_main.h"
using some_obscure_name::QQueue;
zoo.h:
#include "qt_queue.h"
...
QQueue myQueue; // OK
QList myList1; // Error - cannot use QList
some_obscure_name::QList myList2; // No error, but discouraged by Qt developers
声明:我没有使用Qt的经验;这个例子并没有展示Qt开发人员实际做了什么,它只展示了他们可以做什么。
foo。
#ifndef FOO_H
#define FOO_H
typedef int foo_t;
int foo();
class foo_c {};
#endif /* FOO_H */
bar.h
#ifndef BAR_H
#define BAR_H
typedef foo_t bar_t;
int bar();
class foo_c;
class bar_c {
public:
bar_c();
private:
foo_c * my_foo_c;
};
#endif /* BAR_H */
zoo.h
#ifndef ZOO_H
#define ZOO_H
typedef bar_t zoo_t;
int zoo();
class zoo_c {
public:
zoo_c();
private:
bar_c * my_bar_c;
};
#endif /* ZOO_H */
foo.c
#include "foo.h"
int foo() {
return 1;
}
bar.c
#include "bar.h"
#include "foo.h"
int bar() {
return foo();
}
bar_c::bar_c() : my_foo_c(new foo_c()) {}
zoo.c
#include "zoo.h"
#include "bar.h"
int zoo()
{
return bar();
}
zoo_c::zoo_c() : my_bar_c(new bar_c()) {}
介于两者之间的一种方法是引入额外的源文件级别,您可以将其称为.inl
,将函数实现移到那里并使其内联。通过这种方式,您可以将这些新文件包含在原始头文件之后,并且只包含在实际需要的地方,从而获得有限的可见性和最大的内联。不过,我认为不值得花这么大力气。
模板会使事情进一步复杂化,因为一般来说,定义必须在需要实例化模板的任何地方可用。有一些方法可以控制这一点,例如,通过强制实例化所需的专门化,以避免包括每个使用点的定义,但再次增加的复杂性可能不值得。
如果你担心编译时间,通常依赖于编译器头预编译机制要容易得多。
- 如何导出包含具有"std::unique_ptr"值的"std::map"属性的
- 从包含m行的文件中提取n行,必要时(惰性地)重复该文件
- 编译包含字符串的代码时遇到问题
- c++库的公共头文件中应该包含什么
- 将包含C样式数组的对象初始化为成员变量(C++)
- 是否需要删除包含对象的"pair"?
- 函数何时会在c++中包含stack_Unwind_Resume调用
- 如何将包含epoch时间的十六进制字符串转换为time_t
- 使用mongocxx驱动程序时包含头文件问题
- 如何在h文件中包含.o对象文件
- 在混合代码库中将C转换为C++时出现许多包含错误
- VS2017,C++包含目录与附加包含目录,子文件夹包含失败-但为什么
- cmath抛出错误C2062、C2059、C2143和C2447.cmath包含在矢量文件中
- 为什么您需要C++头文件的包含保护
- 无法在UE4中包含BP类到CPP类
- g++ 说函数不存在,即使包含正确的标头
- 在C++代码中包含opencv时,使用ctypes创建.so文件
- Visual C++GC接口如何启用它以及要包含哪个库
- 当调用switch语句中的函数时(即使函数不包含循环),似乎是永不结束的循环的问题
- 如何停止通过分层包含传播声明