C ++ - 需要重新编译时

时间:2010-10-27 13:02:27

标签: c++

您有一个许多库所依赖的类。您需要修改一个应用程序的类。以下哪些更改需要在构建应用程序之前重新编译所有库?

  • 添加构造函数
  • 添加数据成员
  • 将析构函数更改为虚拟
  • 将具有默认值的参数添加到现有成员函数

谢谢

7 个答案:

答案 0 :(得分:30)

类在头文件中定义。头文件将被编译到实现该类的库和使用该类的代码中。我假设您正在考虑在更改类头文件后需要重新编译类实现,并且您要问的问题是您是否需要重新编译引用该类的任何代码。

您所描述的问题是二进制兼容性(BC)之一,通常遵循以下规则:

  1. 在课程的任何地方添加非虚拟功能不会破坏BC。
  2. 更改任何功能定义(添加参数)将破坏BC。
  3. 在任何地方添加虚拟函数都会更改v表,从而破坏BC。
  4. 添加数据成员将破坏BC。
  5. 将参数从非默认值更改为默认值不会破坏BC。
  6. 对内联函数的任何更改都将破坏BC(因此,如果BC很重要,则应避免使用内联函数。)
  7. 更改编译器(有时甚至是编译器版本)可能会破坏BC,除非编译器严格遵守相同的ABI。
  8. 如果BC是您正在实施的平台的主要问题,那么使用Bridge模式分离界面和实现是一个好主意。

    另外,C ++语言不涉及应用程序二进制接口(ABI)。如果二进制兼容性是一个主要问题,您可能应该参考您平台的ABI规范以获取更多详细信息。

    编辑:更新添加数据成员。这将破坏BC,因为现在需要比以前更多的内存。

答案 1 :(得分:13)

严格来说,只要您因任何原因没有重新编译, 就会在Undefined Behavior land 中结束。

那就是说,在实践中你可能会侥幸逃脱:

  
      
  • 添加构造函数
  •   

可以使用

  1. 它不是该类
  2. 的第一个用户定义构造函数
  3. 它不是复制构造函数
  4.   
        
    • 添加数据成员
    •   

    这会更改类实例的大小。对于只使用指针或引用的人来说可能没关系,如果你注意把这些数据放在所有其他数据之后,那么访问其他数据成员的偏移量就不会改变。但是没有定义二进制中子对象的确切布局,因此您将不得不依赖于特定的实现。

      
        
    • 将析构函数更改为虚拟
    •   

    这会更改类的虚拟表,因此需要重新编译。

      
        
    • 将具有默认值的参数添加到现有成员函数
    •   

    由于在调用站点插入了默认参数,因此使用它的每个人都需要重新编译。 (但是,使用重载而不是默认参数可能会让你侥幸逃脱。)

    请注意,任何内联成员函数都可能呈现上述任何错误,因为这些代码的代码直接嵌入(并优化)在客户端代码中。

    然而,最安全的选择是重新编译所有内容。为什么这是一个问题?

答案 2 :(得分:4)

所有这些都需要重新编译使用该类的所有库。 (如果它们包含.h文件)

答案 3 :(得分:4)

sbi的答案非常好(并且应该被评为最高)。但是我认为我可以将“可能确定”扩展为更具体的内容。

  • 添加构造函数

    如果你添加的构造函数是默认构造函数(或者实际上是复制构造函数),那么你必须要小心。如果之前不可用,则它们将由编译器自动生成(因此需要重新编译以确保它们使用已实现的实际构造函数)。出于这个原因,我倾向于总是为构成某些API的类隐藏或定义这些构造函数。

答案 4 :(得分:2)

通过使用序号导出.def文件来维护应用程序二进制接口,在许多情况下可以避免客户端重新编译:

  • 添加构造函数

    将此构造函数导出到 最大的出口表结束 序数词。任何客户端代码 不会调用此构造函数的需要 不编译。

  • 添加数据成员

    如果客户端代码直接操作类对象,而不是通过指针或引用,则这是一个中断。

  • 将析构函数更改为虚拟

    这可能是一个休息,如果你的 class没有任何其他虚拟 功能,这意味着现在你的班级 必须添加一个vptr表并增加 类对象大小和更改内存 layour。如果你的班级已经 有一个vptr表,移动析构函数 到vptr表结束不会影响 对象布局就后向而言 兼容性。但是如果客户端类派生自您的类并且已经定义了自己的虚函数,那么它就会中断。还有任何客户电话 原始的非虚拟析构函数将会中断。

  • 添加一个默认值为的参数 现有的成员函数

    这绝对是一个休息。

答案 5 :(得分:2)

我明显反对@sbi回答:一般来说你需要重新编译。只有在比他发布的情况要严格得多的情况下才能离开。

  
      
  • 添加构造函数
  •   

如果添加的构造函数是默认构造函数或复制构造函数,那么使用隐式定义版本并且不会重新编译的任何代码都将无法初始化对象,这意味着其他方法所需的不变量将不会在构造中设置,即代码将失败。

  
      
  • 添加数据成员
  •   

这会修改对象的布局。即使只使用指针或引用的代码也需要重新编译以适应布局的变化。如果在对象的开头添加成员,则使用该对象的任何成员的任何代码都将被偏移并失败。

struct test { 
   // int x; // added later
   int y;
};
void foo( test * t ) {
   std::cout << t->y << std::endl;
}

如果未重新编译foo,则在取消注释x后,它将打印t->x而不是t->y。如果类型不匹配,甚至会更糟。从理论上讲,即使添加的成员位于对象的末尾,如果有多个访问修饰符,编译器也可以重新排序成员并遇到同样的问题。

  
      
  • 将析构函数更改为虚拟
  •   

如果它是第一个虚拟方法,它将更改对象的布局并获得所有先前的问题以及通过对基础的引用进行删除将调用基础析构函数而不是调度到正确方法的添加。在大多数编译器中(使用vtable支持),它可能意味着类型的vtable的内存布局发生了变化,这意味着可以调用错误的方法并造成破坏。

  
      
  • 添加默认值
  • 的参数   

这是函数签名的更改,以前使用该方法的所有代码都需要重新编译才能适应新的签名。

答案 6 :(得分:0)

只要更改头文件(hpp文件)中的任何内容,就必须重新编译依赖于它的所有内容。

但是,如果更改源文件(cpp文件),则必须重新编译包含此文件中需求定义的库。

打破物理依赖关系的简单方法,即上层的所有库都需要重新编译,就是使用pimpl习语。然后,只要您不触摸头文件,您只需要编译正在修改实现的库。