【问题标题】:If headers of an interface change, why clients of the interface need to be recompiled?如果接口的头发生变化,为什么需要重新编译接口的客户端?
【发布时间】:2015-09-12 22:49:10
【问题描述】:

当我阅读item31的问题陈述:Effective C++最小化文件之间的编译依赖时,下面的陈述让我感到困惑:

class Person {

  public:

     Person(const std::string& name, const Date& birthday,

            const Address& addr);

     std::string name() const;

     std::string birthDate() const;

     std::string address() const;

     ...

  private:

      std::string theName;        // implementation detail

      Date theBirthDate;          // implementation detail

      Address theAddress;         // implementation detail

};

在定义 Person 类的文件中,您可能会找到如下内容:

#include

#include "date.h"

#include "address.h"

不幸的是,这在定义 Person 的文件和这些头文件之间建立了编译依赖关系。如果这些头文件中的任何一个(我的评论:上面列出的头文件,即 、“date.h”、“address.h”)被更改,或者如果它们依赖的任何头文件发生更改,则包含的文件必须重新编译 Person 类,任何使用 Person 的文件也必须重新编译。

我不太明白的是最后突出显示的部分。为什么使用 Person 的客户端需要重新编译?他们只需要重新链接到新编译的 Person 对象代码,对吧(我假设 Person 接口对其客户端保持不变)?

如果客户真正需要的——假设 Person 界面没有改变——只是重新链接,它是否仍然保证 Pimpl 习惯用法?如果任何标头发生更改,Pimpl 类仍需要重新编译。该成语只为客户端节省了一次重新链接。

编辑:似乎对于哪些标题发生了变化有很多困惑。在这种情况下,Scott Meyers 谈到了 Person.h 包含的头文件已更改。但是 Person.h 本身并没有改变,因此使用(#include)Person.h 的客户端看不到任何变化(Person.h 上没有时间戳变化)。 makefile 依赖项将 Person.o 列为先决条件,因此客户端将简单地与新的 Person.o 链接。我正在学习 Pimpl 成语,也许我错过了每个人的论点中的一些明显要点。请说明。

EDIT2:当客户端需要使用Person时,它包含Person.h,其中还包含了所有其他包含的文件,例如date.h和address.h。我错过了这部分,并认为只有 Person.cpp 需要处理这些标头。

【问题讨论】:

  • 您不断重复“标题更改”。我已经多次要求您在您选择的场景中定义它们如何发生变化,但您尚未提供关键细节。

标签: c++


【解决方案1】:

编译中有一个中间步骤。即如果你编译 foo.cpp 并且它包含 a.h ab b.h 然后是一个中间源文件

 a.h content
 b.h content
 foo.cpp content

创建编译的输入。请注意,如果其他标头包含在标头中,它们也会以递归方式列出。 因为标题中的机会导致您的 编译文件,中间文件,change,foo.cpp应该重新编译。

【讨论】:

  • 我假设单独的编译已经到位。类的客户只需要在类接口不变的情况下链接到类对象代码。
  • @Rich 可能你没有很好地封装你的接口类。这是另一个话题。但你的情况很清楚。如果包含的标头有可能您应该重新编译。
  • Person.h 可能包含一些已更改的文件。但是 Person.h 没有改变。这就是我的意思。包括“Person.h”在内的客户端只需要重新链接而不需要编译。
  • @Rich 我建议您仔细阅读我的回答。当你必须生活在地球上时,你在批评重力
  • 我知道您在说 include 是如何工作的。但是客户可能只有一个编译好的目标代码。它所要做的就是重新链接 Person 的新目标文件,而无需经过完整的编译。除非我完全错过了你的意思?
【解决方案2】:

是的,但是如果数据类型大小错误,或者旧代码试图链接到不再存在的代码,则重新链接将失败。这并不神奇:代码在链接时仍然被编译。

您可以在不破坏二进制兼容性的情况下进行部分接口更改;向一个类型添加成员不是该子集的一部分。

(我假设 Person 界面对其客户端保持不变)

这是关键。您的假设已经消除了约束,因此“为什么需要重新编译其他文件”的答案变成了“他们不需要”。

显然,原始上下文中的引用没有提及该假设,这就是它提供更广泛指导的原因。不过,就我个人而言,我希望看到 Meyers 对二进制兼容性进行更深入的解释。

【讨论】:

  • 我假设 Person 类接口保持不变。不添加/删除/编辑成员。
  • @Rich:那您需要告诉我们您所说的“头文件已更改”是什么意思。细节很重要。
  • 具体来说:Scott Meyers 引用的头文件是、“date.h”和“address.h”
  • Person 使用字符串、日期和地址实现。与他们互动是 Person 的工作。我想,Person 的客户不应该担心他们。
  • @Rich:不知道你为什么不回答我的问题。那好吧。那就帮不上忙了。
【解决方案3】:

在一个非常实际的意义上:假设person.h 包含其他文件,或者定义了一些预处理器符号。如果您更改其包含或更改其预处理器符号,则任何还包含 person.h 的文件都可能更改其含义。

实际上,如果我理解正确,编译器将完全重新编译任何受更改影响的编译单元。即使有一些优化可以避免在仅发生“次要”或“无关紧要”的更改时做大量工作,例如添加空格或其他内容,编译器至少需要查看其文本可能按顺序更改的任何编译单元可以肯定。

一般来说,大多数工具链在预处理器扩展后不会缓存每个编译单元的中间结果,即使您使用 ccache 之类的东西,它也不会尝试对缓存的东西做任何智能以避免做当只发生小的变化时工作,它只会尝试检查它是否过时。

因此,更改头文件中的内容可能看起来比更改类的布局或界面更小,但通常仍需要触发重新编译。如果某些编译单元包含像 sizeof 你的班级这样的查询怎么办?或者使用SFINAE技巧来检测它是否有某些方法?

【讨论】:

    【解决方案4】:

    头文件中的核心信息描述了接口。

    函数的接口描述了它的参数(多少、什么类型等)和返回类型。实际的函数实现(定义)需要以预期的方式调用函数 - 并且接口描述了这一点。如果调用函数的代码提供了一组不同的参数,或者如果它的行为好像函数返回的东西与实际不同,那么某处就会出现故障(无论是在函数中,因为它没有给出信息期望,或在调用者中,因为该函数不提供调用者期望的信息)。

    这意味着,如果函数的接口发生变化,那么被调用函数和调用者的代码都需要重新编译,以确保一致性。

    类型定义也是如此。 structclass 类型可能包含成员函数,编译器需要确保这些函数与其调用者的行为之间的一致性(或者程序员必须处理不一致,这可能以棘手的方式表现出来)。此外,在创建类型的实例(即对象或变量)时,编译器需要知道类型的大小(需要多少内存,数组的第二个元素与第一个元素的距离等)以便正确处理对象。

    所有这些信息都在接口中指定,通常放在标题中。是的,如果没有给出信息,编译器可能会做出假设(例如,在 C 中,假设函数返回 int 并在没有事先声明的情况下调用它并接受任意一组参数),但仍然存在不匹配的问题(例如,假设函数返回int,但实际上返回的是某种类型的指针,会发生什么?)。

    更简单地说,构建管理流程(makefile、构建脚本等)通常会检查文件的创建日期。例如,如果相应的对象早于该源文件,或者早于源文件#includes 的任何头文件,则可以重新编译源文件。这样做的逻辑是源文件的内容及其包含的标头会影响编译对象中代码的行为方式,如果目标文件比这些文件中的一个更旧,那么很可能已经发生了变化。使事情对齐的唯一方法是重新编译。

    仅当文件内容发生“实质性”更改时才可能重新编译(例如,如果仅在标题中更改了注释,则不重新编译)。但是,这样做意味着有必要可靠地检测文件中的更改实际上与程序的工作无关。这样做的分析当然是可能的,但通常会更复杂 - 并且耗时,这是一个问题,因为程序员倾向于抱怨构建时间长 - 而不是简单地检查文件日期。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-29
      • 2010-10-25
      • 2010-10-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多