首页 > 解决方案 > 使用不同版本的仅标头库会导致 UB

问题描述

假设我有一个 library somelib.a,它由包管理器作为二进制文件分发。而且这个库使用了只有头文件的库anotherlib.hpp

如果我现在将我的程序链接到somelib.a, 并且还使用anotherlib.hpp但使用不同的版本,那么这可能会导致 UB,如果在其标题中somelib.a使用部分。anotherlib.hppinclude

somelib.a但是,如果仅在其 cpp 文件中引用/使用会发生什么anotherlib.hpp(所以我不知道它使用它们)?将我的应用程序之间的链接步骤,并somelib.a确保somelib.a我的应用程序都将使用他们自己的anotherlib.hpp.

我问的原因是我是否将程序的各个编译单元链接到最终程序,然后链接器会删除重复的符号(取决于它是否是内部链接)。因此,仅标头库通常以可以删除重复符号的方式编写。

一个最小的例子

somelib.a构建在具有 nlohmann/json.hpp 版本 3.2 的系统上

somelib/somelib.h

namespace somelib {
  struct config {
    // some members
  };

  config read_configuration(const std::string &path);
}

一些lib.cpp

#include <nlohmann/json.hpp>


namespace somelib {
  config read_configuration(const std::string &path)
  {
     nlohmann::json j;
     std::ifstream i(path);

     i >> j;

     config c;

     // populate c based on j

     return c;
  }
}

应用程序构建在另一个系统上,nlohmann/json.hpp 版本 3.5 和 3.2 和 3.5 不兼容,然后应用程序链接到somelib.a在版本 3.2 的系统上构建的应用程序

应用程序.cpp

#include <somelib/somelib.h>
#include <nlohmann/json.hpp>
#include <ifstream>

int main() {
   auto c = somelib::read_configuration("config.json");

   nlohmann::json j;
   std::ifstream i("another.json");

   i >> j;

   return 0;
}

标签: c++static-librariesstatic-linking

解决方案


您使用静态库几乎没有任何区别。

C++ 标准规定,如果在一个程序中有多个内联函数(或类模板,或变量等)的定义,并且所有定义都不相同,那么您就有了 UB。

实际上,这意味着除非头库的 2 个版本之间的更改非常有限,否则您将拥有 UB。例如,如果唯一的更改是空格更改、注释或添加新符号,那么您将不会有未定义的行为。但是,如果现有函数中的一行代码被更改,那么它就是 UB。

来自C++17 最终工作草案(n4659.pdf)

6.2 一定义规则

[...]

类类型(第 12 条)、枚举类型(10.2)、带外部链接的内联函数(10.1.6)、带外部链接的内联变量(10.1.6)、类模板(第 17 条)可以有多个定义, 非静态函数模板 (17.5.6), 类模板的静态数据成员 (17.5.1.3), 类模板的成员函数 (17.5.1.1) 或模板特化,其中某些模板参数未在程序,前提是每个定义出现在不同的翻译单元中,并且定义满足以下要求。

给定这样一个名为 D 的实体在多个翻译单元中定义,则

  • D 的每个定义都应由相同的记号序列组成;和

  • 在 D 的每个定义中,根据 6.4 查找的对应名称应指在 D 的定义中定义的实体,或应指同一实体,在重载决议(16.3)和部分模板特化(17.8)匹配之后.3),但名称可以引用 (6.2.1)

    • 一个非易失性 const 对象,如果该对象具有内部链接或没有链接

      • 在 D 的所有定义中具有相同的文字类型,(6.2.1.2)

      • 用常量表达式(8.20)初始化,

      • 在 D 的任何定义中都没有使用 odr,并且

      • 在 D 的所有定义中具有相同的值,

    或者

    • 使用常量表达式初始化的具有内部链接或没有链接的引用,使得该引用在 D 的所有定义中引用相同的实体;(6.3)
  • 在 D 的每个定义中,对应的实体应具有相同的语言链接;和

  • 在 D 的每个定义中,所指的重载运算符、对转换函数、构造函数、运算符新函数和运算符删除函数的隐式调用,应指同一函数,或指 D 定义中定义的函数;和

  • 在 D 的每个定义中,(隐式或显式)函数调用使用的默认参数被视为其标记序列存在于 D 的定义中;也就是说,默认参数受本段中描述的要求的约束(并且,如果默认参数具有带有默认参数的子表达式,则此要求递归适用)。 28

  • 如果 D 是一个具有隐式声明的构造函数 (15.1) 的类,就好像构造函数是在使用它的每个翻译单元中隐式定义的,并且每个翻译单元中的隐式定义应调用相同的构造函数D的子对象。

如果 D 是一个模板并且在多个翻译单元中定义,那么前面的要求既适用于模板定义(17.6.3)中使用的模板封闭范围的名称,也适用于实例化点的依赖名称(17.6.2)。如果 D 的定义满足所有这些要求,那么行为就好像有一个 D的定义。如果 D 的定义不满足这些要求,那么行为是未定义的。


推荐阅读