让我们假设我有一个库somelib.a
,它由包管理器以二进制形式分发。此库使用标题库anotherlib.hpp
。
如果我现在将我的程序与somelib.a
链接,并且还使用anotherlib.hpp
但使用不同的版本,那么这可能导致UB,如果somelib.a
在其anotherlib.hpp
标题中使用include
的部分。
但是如果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
链接
application.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。
实际上,这意味着除非头库的两个版本之间的更改非常有限,否则您将拥有UB。例如,如果唯一的更改是空格更改,注释或添加新符号,那么您将不会有未定义的行为。但是,如果现有函数中的单行代码被更改,则它是UB。
来自C++17 final working draft (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的任何定义中都没有使用过 在D的所有定义中具有相同的值, 要么 具有内部连接或无链接的引用,使用常量表达式初始化,使得引用引用D的所有定义中的相同实体;和(6.3)
- 在D的每个定义中,相应的实体应具有相同的语言链接;和
- 在D的每个定义中,所引用的重载运算符,对转换函数,构造函数,运算符新函数和运算符删除函数的隐式调用,应引用相同的函数,或者引用D定义中定义的函数;和
- 在D的每个定义中,(隐式或显式)函数调用使用的默认参数被视为其标记序列存在于D的定义中;也就是说,默认参数受本段中描述的要求的约束(并且,如果默认参数具有带默认参数的子表达式,则此要求将递归应用).28
- 如果D是一个带有隐式声明的构造函数的类(15.1),就好像构造函数是在每个使用odr的翻译单元中隐式定义的,并且每个翻译单元中的隐式定义应该调用相同的构造函数D.的子对象
如果D是模板并且在多个转换单元中定义,那么前面的要求既适用于模板定义中使用的模板封闭范围中的名称(17.6.3),也适用于实例化时的相关名称。 (17.6.2)。如果D的定义满足所有这些要求,则行为就好像存在D的单个定义。如果D的定义不满足这些要求,那么行为是不确定的。