7

一般问题:

对于非托管 C++,内部代码共享哪个更好?

  1. 通过共享实际源代码来重用代码?或者
  2. 通过共享库/动态库(+所有头文件)重用代码

不管它是什么:你减少重复代码(复制粘贴综合症)、代码膨胀的策略是什么?


具体例子:

以下是我们在我的组织中共享代码的方式:

我们通过共享实际源代码来重用代码。

我们使用 VS2008 在 Windows 上进行开发,尽管我们的项目实际上需要跨平台。我们有许多项目(.vcproj)提交到存储库;有些可能有自己的存储库,有些可能是存储库的一部分。对于每个可交付的解决方案 (.sln)(例如,我们交付给客户的东西),它将 svn:externals 从存储库中获取所有必要的项目 (.vcproj) 以组装“最终”产品。

这很好用,但我很担心最终每个解决方案的代码大小会变得非常大(现在我们的总代码大小约为 75K SLOC)。

另外需要注意的一件事是我们防止所有传递依赖。也就是说,每个不是实际解决方案 (.sln) 的项目 (.vcproj) 都不允许 svn:externals 任何其他项目,即使它依赖于它。这是因为您可能有 2 个项目(.vcproj)可能依赖于同一个库(即 Boost)或项目(.vcproj),因此当您将 svn:externals 两个项目合并为一个解决方案时,svn:externals 将执行两次. 因此,我们仔细记录每个项目的所有依赖项,并由创建解决方案 (.sln) 的人来确保所有依赖项(包括传递性)都是 svn:externals 作为解决方案的一部分。

如果我们通过使用 .lib 、 .dll来重用代码,这显然会减少每个解决方案的代码大小,并在适用的情况下消除上面提到的传递依赖(例外情况是,例如,使用dll 像 Intel TBB 和默认的 Qt)


附录:(如果您愿意,请阅读)

GUI 博士可能最好地总结了共享源代码的另一个动机:

最重要的是,C++ 使简单的不是创建可重用的二进制组件。相反,C++ 使得重用源代码变得相对容易。请注意,大多数主要的 C++ 库都是以源代码形式提供的,而不是编译形式。为了从对象正确继承,经常需要查看该源代码——当您重用它时,依赖原始库的实现细节太容易(而且通常是必要的)。好像这还不够糟糕,修改原始源代码并进行库的私有构建通常很诱人(或有必要)。(有多少 MFC 的私有版本?世界永远不会知道......)

也许这就是为什么当您查看诸如 Intel Math Kernel 库之类的库时,它们的“lib”文件夹中的每个 Visual Studio 版本都有“vc7”、“vc8”、“vc9”。可怕的东西。

或者这个断言怎么样:

C++ 在插件方面是出了名的不适应。C++ 是极其特定于平台和编译器的。C++ 标准没有指定应用程序二进制接口 (ABI),这意味着来自不同编译器甚至同一编译器的不同版本的 C++ 库是不兼容的。除此之外,C++ 没有动态加载的概念,每个平台都提供自己的解决方案(与其他平台不兼容),您就可以了解情况。

您对上述说法有何看法?像 Java 或 .NET 这样的东西会面临这些问题吗?例如,如果我从 Netbeans 生成一个 JAR 文件,如果我将它导入 IntelliJ,只要我确保两者都具有兼容的 JRE/JDK,它会起作用吗?

4

4 回答 4

6

人们似乎认为 C 指定了一个 ABI。它没有,而且我不知道有任何标准化的编译语言可以做到。要回答您的主要问题,使用库当然是要走的路——我无法想象做其他事情。

于 2009-12-11T22:21:03.653 回答
3

分享源代码的一个好理由:模板是 C++ 的最佳特性之一,因为它们是一种绕过静态类型僵化的优雅方式,但本质上是源代码级别的构造。如果您专注于二进制级接口而不是源级接口,那么您对模板的使用将受到限制。

于 2009-12-11T22:59:37.507 回答
1

我们也这样做。如果您需要在不同平台、构建环境上使用共享代码,或者即使您需要不同的构建选项(例如静态与动态链接到 C 运行时、不同的结构打包设置等),那么尝试使用二进制文件可能是一个真正的问题。 .

我通常将项目设置为尽可能多地从源代码按需构建,即使使用诸如 zlib 和 libpng 之类的第三方代码也是如此。对于那些必须单独构建的东西,例如 Boost,我通常必须为所需的各种设置组合(调试/发布、VS7.1/VS9、静态/动态)构建 4 或 8 组不同的二进制文件,并管理二进制文件以及源代码管理中的调试信息文件。

当然,如果每个共享您的代码的人都在同一平台上使用相同的工具和相同的选项,那么情况就不同了。

于 2009-12-11T22:52:44.557 回答
1

我从未将共享库视为将旧项目中的代码重用到新项目中的一种方式。我一直认为更多的是在您同时开发的不同应用程序之间共享一个库,以最大程度地减少臃肿。

就复制粘贴综合症而言,如果我将它复制并粘贴到多个地方,它必须是它自己的功能。这与库是否共享无关。

当我们重用旧项目中的代码时,我们总是将其作为源代码引入。总有一些东西需要调整,调整特定项目的版本通常比调整可能会破坏先前项目的共享版本更安全。回过头来修复之前的项目是不可能的,因为 1) 它已经工作(并已交付),2) 不再有资金支持,以及 3) 所需的测试硬件可能不再可用。

例如,我们有一个通信库,它有一个 API 用于通过套接字、管道等发送“消息”,即带有消息 ID 的数据块:

void Foo:Send(unsigned messageID, const void* buffer, size_t bufSize);

但在后来的项目中,我们需要进行优化:消息需要由连接在一起的内存不同部分中的几个数据块组成,我们不能(也不想,无论如何)做指针数学来创建首先是“组装”形式的数据,并且将这些部分一起复制到统一缓冲区的过程花费了太长时间。所以我们添加了一个新的 API:

void Foo:SendMultiple(unsigned messageID, const void** buffer, size_t* bufSize);

这会将缓冲区组装成一条消息并发送它。(基类的方法分配了一个临时缓冲区,将这些部分复制在一起,然后调用Foo::Send();子类可以将其用作默认值或用自己的方法覆盖它,例如,在套接字上发送消息的类只需为每个部分调用 send()缓冲区,消除大量副本。)

现在,通过这样做,我们可以选择将更改向后移植(实际上是复制)到旧版本,但我们不需要向后移植。这为管理人员提供了灵活性,基于他们所拥有的时间和资金限制。

编辑:阅读尼尔的评论后,我想到了我们需要澄清的事情。

在我们的代码中,我们做了很多“库”。其中很多。我写的一个大程序有大约 50 个。因为,对于我们和我们的构建设置,它们很容易。

我们使用一个工具来自动生成生成文件,处理依赖关系和几乎所有的事情。如果有什么奇怪的事情需要做,我们会写一个包含异常的文件,通常只有几行。

它的工作原理是这样的:该工具在目录中查找所有看起来像源文件的内容,如果文件发生更改,则生成依赖项,并输出所需的规则。然后它制定一个规则,将所有内容和 ar/ranlib 放入以目录命名的 libxxx.a 文件中。所有对象和库都放在一个以目标平台命名的子目录中(这使得交叉编译易于支持)。然后对每个子目录重复此过程(目标文件子目录除外)。然后顶级目录与所有子目录的库链接到可执行文件中,并在顶级目录之后再次创建一个符号链接。

所以目录就是库。要在程序中使用库,请对其进行符号链接。无痛。因此,一切从一开始就被划分为库。如果你想要一个共享库,你可以在目录名称上加上一个“.so”后缀。

要从另一个项目中提取库,我只需使用外部 Subversion 来获取所需的目录。符号链接是相对的,所以只要我不留下任何东西,它仍然有效。当我们发布时,我们将外部引用锁定到父版本的特定版本。

如果我们需要向库添加功能,我们可以做几件事之一。我们可以修改父项(如果它仍然是一个活动项目并且因此可以测试),告诉 Subversion 使用较新的版本并修复任何弹出的错误。或者我们可以克隆代码,替换外部链接,如果与父级混淆太冒险的话。无论哪种方式,它对我们来说仍然看起来像一个“图书馆”,但我不确定它是否符合图书馆的精神

我们正在转向 Mercurial,它没有“外部”机制,因此我们必须首先克隆库,使用 rsync 保持不同存储库之间的代码同步,或者强制使用通用目录结构你可以从多个父母那里得到 hg pull。最后一个选项似乎工作得很好。

于 2009-12-11T22:53:44.917 回答