使用具有不同版本的仅标头库是否会导致 UB

Does the usage of header only libraries with different versions result in UB

本文关键字:是否 UB 版本      更新时间:2023-10-16

假设我有一个库somelib.a,它由包管理器作为二进制分发。并且该库仅使用标头库anotherlib.hpp

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

但是,如果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);
}

Somelib.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 不兼容的系统上,然后将应用程序链接到在具有 3.2 版本的系统上构建的somelib.a

应用.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++标准指出,如果程序中有多个内联函数(或类模板或变量等)的定义,并且所有定义都不相同,那么您就有了 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),就好像构造函数是隐式定义的 在每个使用 ODR 的翻译单元中,以及隐式 每个翻译单元中的定义应调用相同的构造函数 对于 D 的子对象。

如果 D 是模板,并且在多个翻译单元中定义, 则上述要求应同时适用于 模板定义中使用的模板封闭范围 (17.6.3), 以及在实例化点 (17.6.2) 的依赖名称。如果 D的定义满足所有这些要求,然后行为 就好像D有一个单一的定义。 不满足这些要求,则行为是未定义的。