C ++内部代码重用:编译所有内容还是共享库/动态库?

时间:2009-12-11 22:13:32

标签: c++ unmanaged code-reuse

一般问题:

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

  1. 通过共享实际源代码重用代码? OR
  2. 通过共享库/动态库(+所有头文件)重用代码
  3. 无论它是什么:减少重复代码(复制粘贴综合症)的策略是什么,代码膨胀?


    具体示例:

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

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

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

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

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

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


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

    分享源代码的另一个动机可能最好总结为Dr. GUI

      

    最重要的是,C ++变得容易   不创建可重用的二进制文件   组件;相反,C ++成功了   相对容易重用源代码。   请注意,大多数主要的C ++库都是   以源代码形式发送,未编译   形成。经常需要这样做   看看那个来源是为了   从对象中正确继承 - 和   这太容易了(而且经常   必要的)依靠实施   原始图书馆的详细信息   你重复使用它。好像那还不错   足够的,它往往是诱人的(或   必要的)修改原件   来源并做一个私人构建   图书馆。 (有多少私人版本   MFC在那里?世界永远不会   知道。 。 。)

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

    this断言如何:

      

    众所周知,C ++是不适应的   当谈到插件。 C ++是   非常特定于平台和   编译器特有的。 C ++标准   没有指定应用程序二进制文件   接口(ABI),表示C ++   来自不同编译器或库的库   甚至不同版本的相同   编译器不兼容。加上那个   C ++没有概念的事实   动态加载和每个平台   提供自己的解决方案(不兼容   与其他人一起)你得到了照片。

    您对上述主张有何看法?像Java或.NET这样的问题会遇到这些问题吗?例如如果我从Netbeans生成一个JAR文件,只要我确保它们都兼容JRE / JDK,它是否可以导入IntelliJ?

4 个答案:

答案 0 :(得分:6)

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

答案 1 :(得分:3)

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

答案 2 :(得分:1)

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

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

当然,如果每个共享代码的人都使用相同的工具在同一平台上使用相同的选项,那么这是一个不同的故事。

答案 3 :(得分: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()为每个缓冲区,消除了大量的副本。)

现在,通过执行此操作,我们将选项向后移植(实际上是复制)旧版本的更改,但我们不是必需来向后移植。根据他们的时间和资金限制,这为管理者提供了灵活性。

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

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

我们使用一种工具,可以动态自动生成makefile,负责依赖性和几乎所有内容。如果需要做任何奇怪的事情,我们会编写一个带有异常的文件,通常只有几行。

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

所以目录是库。要在程序中使用库,请为其创建符号链接。无痛。因此,从一开始就将所有内容划分为库。如果需要共享库,则在目录名称上添加“.so”后缀。

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

如果我们需要向库中添加功能,我们可以执行以下操作之一。我们可以修改父级(如果它仍然是一个活动的项目,因此是可测试的),告诉Subversion使用更新的版本并修复弹出的任何错误。或者我们可以克隆代码,替换外部链接,如果弄乱父母太冒险了。无论哪种方式,它仍然看起来像我们的“库”,但我不确定它是否与库的精神相匹配。

我们正在迁移到没有“外部”机制的Mercurial,所以我们必须首先克隆库,使用rsync来保持代码在不同的存储库之间同步,或强制共同目录结构,这样你就可以从多个父项中获取hg。最后一个选项似乎运作良好。