如何通过分层包含停止传播声明?

每当我制作.h头文件时,我都会想到一个问题: “如何通过分层包含停止传播声明?” 假设有以下文件:

foo.h中

#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_cfoo_tfoo() ,并且对foo.h的每次更改都将重新编译 zoo.h。

我知道我们可以将实现移动到.cpp文件,但是在.h文件中的类定义中编写的代码怎么样? 如果需要,我们如何强制程序员在zoo.h中明确地包含foo.h

作为Qt中的一个例子,当我包含并使用 ,我无法访问QList ,其中QQueueQListinheritance,我必须明确地包含(另外,我不知道它是如何完成的,它对编译时的影响)

在C ++和C中,“要停止传播声明”,您需要将它们从公共接口,句点中删除。 将它们移动到实现。 或者“少公开”的界面。

编译时间是目标之一。 其他是便携性,可维护性。 这也与松耦合直接相关。

Pimpl成语是最受欢迎的C ++技术,它可以帮助您进行类推导。 派生您的实现类,在公共接口中包含相应的头文件到实现cpp和forward-declare实现。 您的用户对基类一无所知,只会知道您的实现名称。

如果你想使用typedef就不可能停止传播。 但是为了提供更好的可移植性和可维护性,您可以使用与Boost库有效使用相同的方法:实现定义类型(例如, 此类型)。

每个界面设计都是可扩展性 , 信息隐藏和简单性(或努力)之间的权衡。 如果你需要先归档两个使用更复杂的方法。 您可以提供两个公共接口:一个用于使用,另一个用于更宽和更低级别,以实现可扩展性。

我发现在我的代码中明确区分前向声明与定义很重要: 尽可能使用前向声明

一般来说,如果你的类X不需要知道类Y的sizeof,你需要的只是Y的前向声明 – 你不需要包含Y.hpp。

例如,如果X不是Y的子类,而X不包含Y类型的任何成员,那么您不需要包含Y.hpp。 前向声明Y类; 足够了。 有时,为了更好地解耦我的代码,我将持有Y的引用或指针而不是在类X中嵌入Y – 如果这是可行的,那么我需要做的就是前向声明类Y;

现在,有一条关于在使用模板类时无法转发声明的注释。 但是有一个诀窍 – 而不是使用typedef,你想要的模板实例化的子类,例如:

 class Bars : public std::vector { }; 

现在你可以向前声明class Bars; ,之前你无法转发声明std::vector;

所以,这些是我在所有C ++项目中遵循的步骤:

  1. 将我的代码分成由命名空间划分的模块
  2. 在每个模块中创建一个fdecl.hpp文件,该文件包含该模块的前向声明
  3. 非常喜欢在#include 上使用#include (对定义的前向声明)

通过这种方式,标头松散耦合,我修改代码时编译时间更快。

我会用这种方式重写代码:

foo.h中

 #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.h中

 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.h中

 #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 ,在那里移动函数实现并使它们内联。 通过这种方式,您可以在原始标题之后包含这些新文件,并且仅在实际需要的位置包含这些新文 不过,我认为这不值得。

模板会使事情进一步复杂化,因为通常定义必须在模板需要实例化的任何地方都可用。 有一些方法可以控制这种情况,例如通过强制实现所需特化的实例化,以避免包含每个使用点的定义,但同样增加的复杂性可能不值得。

如果您担心compilaton时间通常会更容易依赖编译器的头预编译机制。