使用共享库还是单一可执行文件?

8
我的同事认为我们应该将我们的C++应用程序(C++, Linux)分解成共享库,以提高代码的模块化、可测试性和重用性。
但我认为这会增加负担,因为我们编写的代码既不需要在同一台机器上的应用程序之间共享,也不需要动态加载或卸载,我们可以简单地链接一个单体可执行应用程序。
此外,用C函数接口包装C++类会使它变得更丑陋。
我还认为,单个文件的应用程序在客户现场进行远程升级会更容易。
当没有必要在应用程序之间共享二进制代码,也没有动态代码加载时,是否应该使用动态库?

2
不,它并不是这样的。这更像是一个可以区分好坏答案的问题。因此,写出好答案的人应该得到声望奖励。当所有答案都相等时(比如在一次投票或者开玩笑的讨论中),使用CW(社区维基)。 - jalf
1
在组件之间设置硬边界以防止意外或懒惰的依赖创建是有道理的。在您的情况下,将应用程序拆分为静态链接库即可,这样您可以保留单个可执行文件并仍具有独立的编译单元。 - Hans
2
任何东西都不需要编写 C 包装器。 - Martin York
10个回答

9
我认为,将代码拆分为共享库而没有任何明确目标的做法表明开发环境已经充斥着时髦词汇。更好的方法是编写可以轻松拆分的代码。
但是,除了对象创建之外,为什么需要将C ++类包装成C函数接口呢?
此外,在这里拆分成共享库听起来像是一种解释性语言思维方式。在编译型语言中,您尽量不要推迟到运行时可以在编译时完成的工作。不必要的动态链接就是这种情况。

编译器的不兼容性使得在共享库接口中不使用任何C++特性更加容易。例如,如果类的布局在客户端实现时有所不同(例如,对齐方式不同),则由他们的编译器编译的代码会导致应用于您的数据结构时出现错误。还有一些旧编译器存在虚函数表问题。 - Basilevs
1
Basilevs,这通常是正确的,但就我所理解的情况而言,它根本不适用。 - Michael Krelin - hacker
@Bavilevs: 解决方案是提供两个DLL。一个是C ++ DLL,由客户端代码在同一编译器上使用,另一个是为第一个DLL提供C接口的DLL。没有必要通过强制实施C接口来惩罚共享相同编译器的客户端。 - paercebal
@Basilevs:黑客是正确的:在当前情况下,编译器已经是所有静态库的已知且唯一的。我认为如果他们尝试“共享库”解决方案,这种情况不会改变。因此,基本上,在共享库之间没有需要C接口的必要。在我看来,这个“C接口”论点表明问题作者对共享库不熟悉。 - paercebal

5
强制使用共享库可以确保库不具有循环依赖关系。使用共享库通常会导致更快的链接,并且在最终应用程序链接之前发现链接错误比没有任何链接要早。如果您想避免向客户发送多个文件,可以考虑在开发环境中动态链接应用程序,在创建发布版本时静态链接。
编辑:我真的看不出为什么需要使用C接口来包装C++类-这是在幕后完成的。在Linux上,您可以使用共享库而无需进行任何特殊处理。但是,在Windows上,您需要使用___declspec(export)和___declspec(import)。

在某些系统上,您可以拥有具有未解析符号的共享库。因此,可以创建一组共享库,以便它们必须全部链接到应用程序中。 - KeithB

4

即使没有任何重复使用,也可以提高重复使用率吗?听起来不像是一个有力的论点。

代码的模块化和可测试性不需要依赖于最终部署的单元。我期望链接是一个晚期决策。

如果你真的只有一个可交付成果,并且从未预计对其进行任何更改,那么分阶段交付似乎过于繁琐和不必要的复杂性。


2

简短回答:不需要。

更详细的回答:动态库与单体应用程序相比,对测试、模块化或重用并没有额外的好处。我唯一能想到的好处是可能会迫使一个团队在没有自律的情况下创建API。

库(无论是静态还是动态)本身并没有什么神奇之处。如果您拥有构建应用程序所需的所有代码和各种库,则可以轻松地将它们全部编译为单个可执行文件。

总的来说,我们发现,除非有强烈的需求(多个应用程序中的库,需要更新多个应用程序而无需重新编译,使用户能够向应用程序添加功能),否则处理动态库的成本是不值得的。


2

分析同事的观点

如果他认为将你的代码拆分为共享库会提高代码的模块化、可测试性和重用性,那么我想这意味着他认为你的代码存在一些问题,并且强制执行“共享库”架构将改正它。

模块化?

您的代码必须具有不希望发生的相互依赖关系,若在“库代码”和“使用库代码的代码”之间进行更清晰的分离,则这种情况将不会发生。

现在,这也可以通过静态库实现。

测试?

您的代码可能需要更好地进行测试,例如为每个单独的共享库构建单元测试,在每次编译时自动化执行。

现在,这也可以通过静态库实现。

代码重用?

您的同事想要重用某些未公开的代码,因为它隐藏在您的单块应用程序的源代码中。

结论

第1点和第2点仍然可以通过静态库实现。第3点将使共享库成为必需。

现在,如果您有多层库链接(我正在思考将两个已经编译链接其他库的静态库链接在一起),这可能会很复杂。在Windows上,这会导致链接错误,因为某些函数(通常是C/C++运行时函数,当静态链接时)被引用多次,编译器无法选择要调用的函数。我不知道Linux上如何工作,但我想这也可能发生。

分析自己的观点

您自己的论点有些有偏见:

编译/链接共享库的负担?

与编译/链接静态库相比,编译/链接共享库的负担不存在。因此,这个论点没有价值。

动态加载/卸载?

在极少数情况下,动态加载/卸载共享库可能是一个问题。在正常情况下,操作系统会在需要时加载/卸载库,而且,您的性能问题在其他地方。

公开C++代码和C接口?

至于将C ++代码使用C函数接口,我不能理解:您已经使用C ++接口将静态库链接在一起。链接共享库没有任何区别。

如果您需要使用不同的编译器来生成应用程序中的每个库,那么您将遇到问题,但这不是问题,因为您已经将库静态链接在一起。

单个文件二进制更容易?

您是正确的。

在Windows上,差异微不足道,但这就是DLL地狱的问题,如果将版本添加到库名称中或使用Windows XP,则问题消失。

在Linux上,除了上述的Windows问题之外,你还需要知道默认情况下共享库需要在一些系统默认目录中才能使用,所以你需要在安装时将它们复制到那里(这可能会很麻烦...)或更改一些默认环境设置(这也可能很麻烦...)
结论:谁是正确的?
现在,你的问题不是“我的同事是对的吗?”。他是正确的。你也是。
你的问题是:
1. 你真正想要实现什么? 2. 这项工作是否值得做?
第一个问题非常重要,因为在我看来,你和你的同事的论点都有偏见,以导致每个人都认为自己的观点更自然的结论。
换句话说:你们每个人已经知道理想解决方案应该是什么(根据每个观点),并且你们每个人都堆积了论点来达成这个解决方案。
没有办法回答这个隐藏的问题...
^_^

tl;dra:(太长了,但还是读了)OP没有暗示他们正在链接静态库,没有这样的提示,应该假定他们通过链接大量目标文件来生成可执行文件。此外,我认为OP可能有偏见,甚至下意识地混淆静态库和共享库;我很难相信他的同事不知道使用静态库可以实现他想要的大部分好处。 - just somebody

1

共享库带来了一些麻烦,但我认为在这里使用共享库是正确的选择。我认为在大多数情况下,您应该能够使应用程序的某些部分模块化并可重用于业务的其他地方。此外,根据这个庞大的可执行文件的大小,只需上传一组更新的库可能会更容易,而不是一个大文件。

在我看来,通常情况下,库可以导致更好的代码、更易于测试的代码,并且允许未来的项目以更高效的方式创建,因为您不必重新发明轮子。

简而言之,我同意您的同事的观点。


2
但是,将可能永远不会被重用的代码模块化有何意义呢? 我同意进行模块化,但只有在值得付出努力的情况下。 - Shirkrin
1
我认为在大多数情况下,模块化都是值得努力的,尤其是在谈论“单块式”应用程序时。我不认为将代码编译到库中并在我的代码中使用会太耗费时间。虽然库没有什么神奇之处,但我觉得它们有助于使依赖关系更加明显,提高可测试性,加快编译速度并且创造更好的环境。如果以后代码会再次被使用,那么是何时决定将其制作为库?程序员会花时间去做还是只是将文件复制到他/她的项目中并进行编译?早期进行处理可以避免混乱。 - RC.

1

进行简单的成本/效益分析 - 您真的需要模块化、可测试性和重用性吗?您有时间花费重构代码来获得这些功能吗?最重要的是,如果您确实进行了重构,您获得的好处是否能够证明执行重构所花费的时间是值得的?

除非您现在存在测试问题,否则我建议将应用程序保留为原样。模块化很棒,但Linux有自己的“DLL地狱”版本(请参见ldconfig),而且您已经表明重用不是必需品。


谁不需要可测试性? - just somebody

1
在Linux(和Windows)上,您可以使用C ++创建共享库,而无需使用C函数导出进行加载。
也就是说,您将classA.cpp构建为classA.so,将classB.cpp构建为链接到classA.so的classB(.exe)。您真正做的是将应用程序拆分为多个二进制文件。这样做的优点是编译速度更快,易于管理,并且您可以编写仅加载该库代码以进行测试的应用程序。
一切仍然是C ++,一切都链接在一起,但是您的.so与静态链接的应用程序分开。
现在,如果您想在运行时加载不同的对象(也就是说,在运行时您不知道要加载哪个对象),那么您需要创建具有c-exports的共享对象,但是您还需要手动加载这些函数;您将无法使用链接器来为您执行此操作。

1

如果你提出问题,而答案并不明显,那么就留在原地。如果你还没有达到构建单体应用程序需要太长时间或者对于你的团队来说太麻烦的程度,那么没有必要转向库。如果你想要的话,可以构建一个测试框架,它可以在应用程序的文件上工作,或者你可以简单地创建另一个项目,使用相同的文件,但附加一个测试API,并构建一个库。

为了运输目的,如果你想要构建库并且发货一个大型可执行文件,你总是可以静态链接到它们。

如果模块化有助于开发,即你总是与其他开发人员在文件修改方面发生冲突,那么库可能会有所帮助,但这也不是保证。无论如何,使用良好的面向对象的代码设计都会有所帮助。

没有理由包装任何函数与C可调用接口必要创建一个库,除非你希望它可以从C中调用。


0
首先,让我澄清你问题中的错误假设。你不需要使用C接口来包装你的C++代码。
现在让我们进入正题。
缺点: - 将你的应用程序拆分成模块是一项工作。 - 你可能会发现相互依赖关系,这会使它变得更加困难。 - 你将不得不确保库在运行时进入正确的位置。Windows上与exe同一目录,Linux/*nix上则放在LD_LIBRARY_PATH中的某个位置。
非问题: - 性能。一旦加载,调用dll中的函数应该没有性能惩罚。在某些系统上甚至可以提高内存效率,因为加载程序可以在不需要时卸载东西/写时复制等。
优点:
  • 模块化。如果功能区域被清晰地分离,就可以轻松地在多个开发人员之间分配工作。
  • 测试。一个模块可以在隔离的情况下进行测试(通过单元测试),而不必担心其他代码区域。
  • 认知负荷。当你编写应用程序时,很难维护整个应用程序的心理图像。两年后,当你回来升级它时,这甚至更难。想象一下对于新开发人员来说会是什么样子。

结论:

  • 我喜欢模块,但这取决于你的情况。如果你距离交货只有一周时间,大部分事情都正常工作,并且你有每个前景修复少数剩余的小错误 - 为什么现在要改变呢。
  • 另一方面,如果你正在与棘手的错误斗争,有些代码在改变无关事项时神秘地崩溃,或者处于开发的中期并且落后于计划,将其分解成模块可以减少复杂性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接