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

How to stop propagating declarations through hierarchical includes?

本文关键字:包含 传播 声明 分层 何停止      更新时间:2023-10-16

每当我创建.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

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

作为Qt中的一个例子,当我包括和使用<QQueue>时,我无法访问QList,其中QQueueQList继承,我必须显式地包括<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++项目中遵循的步骤:

  1. 将我的代码分成按命名空间划分的模块
  2. 在每个模块中创建一个 fdecle .hpp文件,该文件包含该模块的转发声明
  3. 强烈建议使用#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,将函数实现移到那里并使其内联。通过这种方式,您可以将这些新文件包含在原始头文件之后,并且只包含在实际需要的地方,从而获得有限的可见性和最大的内联。不过,我认为不值得花这么大力气。

模板会使事情进一步复杂化,因为一般来说,定义必须在需要实例化模板的任何地方可用。有一些方法可以控制这一点,例如,通过强制实例化所需的专门化,以避免包括每个使用点的定义,但再次增加的复杂性可能不值得。

如果你担心编译时间,通常依赖于编译器头预编译机制要容易得多。