当两个共享库定义相同的符号时实际上会发生什么?

eos*_*rox 6 c++ shared-libraries

我最近在将两个共享库(都是我自己制作的)链接在一起时遇到了崩溃问题。我最终发现这是因为两个文件之间有一个源文件重复。在该源文件中定义了一个全局 std::vector (实际上是类的静态成员),它最终被释放两次——每个库释放一次。

然后我写了一些测试代码来验证我的想法。在标头中,我声明了一个类和该类的全局对象:

#ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_

#include <iostream>

struct Data {
  Data(void) {std::cout << "Constructor" << std::endl;}
  ~Data(void) {std::cout << "Destructor" << std::endl;}
  int FuncDefinedByLib(void) const;
};

extern const Data data;

#endif
Run Code Online (Sandbox Code Playgroud)

FuncDefinedByLib函数未定义。然后我创建了两个库,libA并且libB都包含这个标头。 libA看起来像这样

const Data data;

int Data::FuncDefinedByLib(void) const {return 1;}

void PrintA(void) {
  std::cout << "LibB:" << &data << " "
    << (void*)&Data::FuncDefinedByLib <<  " "
    << data.FuncDefinedByLib() << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

它定义了全局data对象、函数以及打印对象地址、 的地址和 的返回值的FuncDefinedByLib函数。PrintAdataFuncDefinedByLibFuncDefinedByLib

libB几乎相同,libA只是名称PrintA更改为PrintBFuncDefinedByLib返回 2 而不是 1。

然后我创建一个链接到这两个库并调用PrintA和 的程序PrintB。在遇到崩溃问题之前,我认为这两个库都会创建自己的class Data. 然而实际输出

Constructor
Constructor
LibB:0x7efceaac0079 0x7efcea8bed60 1
LibB:0x7efceaac0079 0x7efcea8bed60 1
Destructor
Destructor
Run Code Online (Sandbox Code Playgroud)

表示两个库仅使用一个版本,即使类和对象的定义不同,也class Data仅使用一个版本,这是来自(我理解这是因为首先链接)。双重破坏清楚地解释了我的崩溃问题。const Data datalibAlibA

这是我的问题

  1. 这是怎么发生的?我了解链接两个库的主代码可能仅链接到它看到的第一个符号。但是共享库应该在创建时在内部链接(或者不是?我真的对共享库没有太多了解),他们怎么知道其他库中有一个孪生类,并在创建后链接到该类是他们自己创建的?

  2. 我知道共享库之间存在重复代码通常是一种不好的做法。但是是否存在一个条件,通过满足它,库之间的复制是安全的?或者有没有一种系统的方法可以让我的代码可以无风险地复制?或者它永远不安全并且应该始终被严格禁止?我不想总是为了共享一小段代码而分割另一个共享库。

  3. 这种行为看起来很神奇。有人利用这种行为来做一些神奇的事情吗?

J. *_*rez 3

第 1 部分:关于链接器

这是 C 和 C++ 中的一个已知问题,也是当前编译模型的结果。对它如何发生的完整解释超出了本答案的范围,但是Matt Godbolt 的演讲为初学者提供了对该过程的深入解释。另请参阅这篇关于链接器的文章

2020 年将推出新版本的 C++,它将引入新的编译模型(称为模块)来避免此类问题。您将能够从模块导入和导出内容,类似于 Java 中包的工作方式。

第 2 部分:解决您的问题

有几种不同的解决方案。

神奇的解决方案 1:一个唯一的全局变量

这个相当圆滑啊 如果将全局变量作为静态变量粘贴在函数内,则它始终只会构造一次,并且这是由标准确保的(即使在多线程环境中)。

#ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_

#include <iostream>

struct Data {
  Data(void) {std::cout << "Constructor" << std::endl;}
  ~Data(void) {std::cout << "Destructor" << std::endl;}
  int FuncDefinedByLib(void) const;
};
Data& getMyDataExactlyOnce() {
    // The compiler will ensure
    // that data only gets constructed once
    static Data data;
    // Because data is static, it's fine to return a reference to it
    return data; 
}

// Here, the global variable is a reference
extern const Data& data = getMyDataExactlyOnce();

#endif
Run Code Online (Sandbox Code Playgroud)

神奇的解决方案 2:多个不同的全局变量,每个翻译单元 1 个

如果在 C++17 中将全局变量标记为内联,则包含标头的每个翻译单元都会在内存中自己的位置获取自己的副本。请参阅: https: //en.cppreference.com/w/cpp/language/inline

#ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_

#include <iostream>

struct Data {
  Data(void) {std::cout << "Constructor" << std::endl;}
  ~Data(void) {std::cout << "Destructor" << std::endl;}
  int FuncDefinedByLib(void) const;
};
// Everyone gets their own copy of data
inline extern const Data data;

#endif
Run Code Online (Sandbox Code Playgroud)

第三部分:我们可以用它来做黑魔法吗?

有点儿。如果你真的真的想用全局变量做黑暗魔法,C++14 引入了模板化全局变量:

template<class Key, class Value>
std::unordered_map<Key, Value> myGlobalMap; 

void foo() {
    myGlobalMap<int, int>[10] = 20;
    myGlobalMap<std::string, std::string>["Hello"] = "World"; 
}
Run Code Online (Sandbox Code Playgroud)

随你所欲。我对模板化全局变量没有太多用处,尽管我想如果您正在执行诸如计算调用函数的次数或创建类型的次数之类的操作,那么这样做会很有用。