【问题标题】:How to stop propagating declarations through hierarchical includes?如何停止通过分层包含传播声明?
【发布时间】:2011-12-11 02:21:42
【问题描述】:

每当我创建 .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 中的示例,当我包含和使用 <QQueue> 时,我无法访问 QList,其中 QQueueQList 继承,我必须明确包含 <QList>(另外,我不知道它是如何完成的,以及它对编译时间的影响)

【问题讨论】:

  • 您对 protect 的使用表明有些东西需要保护。防止什么?有什么问题?
  • @RobKennedy:阅读有关编译时间的更新问题。
  • 我不确定这是否有帮助,但我有一个减少标题依赖的技巧。如果你只使用指针或引用的类型,你可以直接声明它而不是包含它的头。
  • 如果 QQueue 继承自 QList,那么我向您保证,每次使用 QQueue 时,您还编译了 QList 的所有代码,无论您是否明确包含 qlist.h。如果您认为我错了,请发布演示您要解决的问题的代码。
  • 您通常通过分离声明和定义来保护自己免受标题更改的影响。 (假设函数声明的更改频率远低于实现。) - 如果您在标头中实现所有内容,那么您基本上会得到一个包含所有内容的编译单元。

标签: c++ c header-files


【解决方案1】:

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

编译时间是目标之一。其他的是便携性、可维护性。这也与loose coupling 直接相关。

可以帮助您进行类派生的最流行的 C++ 技术是Pimpl idiom。派生您的实现类,将相应的标头包含到实现 cpp 中,并在您的公共接口中前向声明实现。您的用户对基类一无所知,只会知道您的实现名称。

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

每个界面设计都是extensibilityinformation hiding 和简单(或努力)之间的权衡。如果您需要存档前两个,请使用更复​​杂的方法。您可以提供两个公共接口:一个用于使用,另一个用于更广泛和更低级别的可扩展性。

【讨论】:

    【解决方案2】:

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

    一般来说,如果你的 X 类不需要知道 Y 类的大小,你只需要一个 Y 的前向声明——你不需要包含 Y.hpp。

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

    现在,有一条关于在使用模板类时无法进行前向声明的评论。但是有一个技巧 - 不是使用 typedef,而是您想要的模板实例化的子类,例如:

    class Bars : public std::vector<Bar> { };
    

    现在您可以转发声明class Bars;,而以前您不能转发声明std::vector&lt;Bar&gt;;

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

    1. 将我的代码分成按命名空间划分的模块
    2. 在每个模块中创建一个 fdecl.hpp 文件,其中包含该模块的前向声明
    3. 强烈倾向于使用 #include &lt;modulename/fdecl.hpp&gt; 而不是任何 #include &lt;modulename/foo.hpp&gt;(前向声明优于定义)

    通过这种方式,头文件是松散耦合的,我在修改代码时获得了更快的编译时间。

    【讨论】:

      【解决方案3】:

      我会这样重写代码:

      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();
      }
      

      动物园.h

      #ifndef ZOO_H
      #define ZOO_H
      
      inline int zoo();
      
      #endif  /* ZOO_H */
      

      动物园.cpp

      #include "zoo.h"
      #include "bar.h"
      
      inline int zoo()
      {
          // cannot *incidentally* access foo() here, explicit #include "foo.h" needed
          return bar();
      }
      

      这样你只在头文件中公开你的接口,实现细节保留在 .cpp 文件/中。

      但请注意,如果您使用模板,此策略将失败:必须在标头中完整声明它们(否则您可能会遇到链接器问题)。

      【讨论】:

      • 这不会阻止任何东西访问foo(),但可能会阻止它被内联。
      • 无效,也可以在zoo.h中访问foo()
      • 好的,但是考虑一下typedef 或您需要在 bar.h 中使用的一些声明
      • @Masoud:在 zoo.h 中,您仅 #include bar.h,因此您无法直接访问 foo()(除非您“手动”包含 foo.h)。
      • @Mike:您无法阻止恶意同事使用该功能。毕竟,他可能会在 Windows 上使用 LoadLibrary/GetProcAddress 或一些类似的低级技巧。您可以做的是在其他人包含您的标题时排除内部函数自动可见。
      【解决方案4】:

      也许你可以使用命名空间:

      foo.h

      namespace f {
          inline int foo();
      }
      

      bar.h

      #include "foo.h"
      inline int bar()
      {
          using namespace f;
          return foo();
      }
      

      动物园.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;
      

      动物园.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 开发人员实际做了什么,它只展示了他们可以做什么。

      【讨论】:

        【解决方案5】:

        你不能一边吃蛋糕一边吃。要么尽可能多地利用内联,要么尽可能地限制可见性。对于类,您必须在使用派生和/或直接数据成员之间取得平衡,这需要相应的类定义可用,或间接数据成员,即指针或引用,只需要声明类。您的方法倾向于内联/直接包含,相反的极端是:

        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,将函数实现移到那里并使其内联。通过这种方式,您可以在原始标题之后包含这些新文件,并且仅在实际需要的地方包含这些新文件,并获得有限的可见性和最大的内联。不过,我认为这不值得。

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

        如果您担心编译时间,通常依赖编译器的头文件预编译机制会容易得多。

        【讨论】:

        • 选择内联函数是我的错误,我应该用类声明来写我的问题。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-10-27
        • 2022-01-15
        • 1970-01-01
        • 2015-03-24
        • 2014-09-29
        相关资源
        最近更新 更多