如果接口的头改变,为什么需要重新编译接口的客户端

If headers of an interface change, why clients of the interface need to be recompiled?

本文关键字:接口 新编译 客户端 编译 改变 为什么 如果      更新时间:2023-10-16

当我阅读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 & lt;string>

# include"date.h"

# include"address.h"

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

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

如果客户真正需要的——假设Person接口没有改变——只是一个重链接,那么它还能保证使用Pimpl习语吗?如果任何头文件发生变化,Pimpl类仍然需要重新编译。这个习惯用法只为客户端保存一次重链接。

EDIT:似乎有很多关于头已经改变的困惑。在这种情况下,Scott Meyers谈论的是Person.h所包含的头文件被更改。但是Person.h本身不会改变,所以使用(# include) Person.h的客户端不会看到更改(Person.h上没有时间戳更改)。makefile依赖项将列出Person。o作为先决条件,因此客户端将简单地链接到新的person。我正在学习青春痘成语,也许我在每个人的争论中都错过了一些明显的要点。请阐明。

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

在编译过程中有一个中间步骤。例如,如果你编译foo.cpp,它包含a.h ab b.h,那么中间源文件

 a.h content
 b.h content
 foo.cpp content
创建

,它是编译的输入。注意,如果其他头文件包含在头文件中,它们也会递归列出。因为头球的机会导致你编译文件,中间文件,change, foo.cpp应重新编译。

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

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

(我假设Person接口与其客户端保持相同)

这是钥匙。你的假设已经消除了约束,所以"为什么其他文件需要重新编译"的答案变成了"他们不需要"。

显然,这句话在其原始上下文中并没有提到这个假设,这就是为什么它给出了更广泛的指导方针。不过,就我个人而言,我更希望看到Meyers对二进制兼容性进行更深入的解释。

在非常实际的意义上:假设person.h包含其他文件,或者定义一些预处理器符号。如果你改变了它的include或者它的preprocessors符号,那么任何包含person.h的文件都有可能改变它的含义。

如果我理解正确的话,实际上编译器将完全重新编译受更改影响的任何编译单元。即使有一些优化,以避免在只有"小"或"不重要"的变化发生时做大量的工作,如添加空白或其他,编译器至少需要查看其文本可能被更改的任何编译单元,以确保。

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

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

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

函数的接口描述了它的参数(有多少,什么类型等)和返回类型。实际的函数实现(定义)要求以预期的方式调用函数——接口对此进行了描述。如果调用函数的代码提供了一组不同的参数,或者如果它表现得好像函数返回了与实际不同的东西,那么就会在某个地方出现故障(要么在函数中,因为它没有给出它所期望的信息,要么在调用者中,因为函数没有给出调用者所期望的信息)。

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

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

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

更简单地说,构建管理过程(makefiles、构建脚本等)通常检查文件的创建日期。例如,如果一个源文件对应的对象比该源文件早,或者比源文件#include的任何头文件都早,那么该源文件可能会被重新编译。这样做的逻辑是,源文件的内容及其包含的头文件会影响编译对象中的代码的行为,如果目标文件比这些文件中的一个更老,那么很可能已经发生了变化。唯一的办法就是重新编译。

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