• C++模板编程(21)---C++实例化实现方案implementation Schemes


    以下以obj 表示目标文件object file, lib表示库library,exe表示可执行程序executable files。

    回顾流行的C++编译器产品实现置入式模型inclusion model方法。所有这些产品都依赖两个典型组件:编译器和链接器。编译器用来将源码转翻译为obj文件,其中包含带符号标注(symbolic annotations; 用以交叉引用其它obj文件和lib文件)的机器码。链接器用来合并obj文件、解析obj文件所含的符号式交叉引用symbolic cross-reference,最终生成exe文件或lib文件。接下来的内容中,我们假设你的编译系统使用这种模型,当然以其它方式实现C++编译系统也是完全可能的。例如可以想象一个C++直译器interpreter。

        当一份class template 特化体被用于多个编译单元中,编译器会在处理每个编译单元时重复具现化。这不会引发什么问题。因为编译器并不直接根据class定义式产生机器码。C++编译器只是在其内部用到这些具现化,以便检验或解释其他各种算式和声明。从这个角度说,class定义式的多次实例化,与class定义式在多个编译单元被多次含入(典型情况是透过含入文件)并无本质区别。

        然而,如果你实例化一个noninline function template。情况可能有所不同。如果你提供了一个常规的noninline function 的多份定义,将违反ODR(单一定义原则)。假设你要编译并链接一个程序,而它由下面两个文件组成:

    // a.cpp

    int main()

    {

    }

    // b.cpp

    int main()

    {

    }

    C++ 编译器会顺利编译两个文件,不会遭遇任何问题,因为它们确实都是合法的C++编译单元。然而如果你试图链接两个obj文件,链接器会报错,因为在同一个程序中重复定义了main()函数,这是不允许的。

    对比地,考虑template的情况:

    //t.hpp

    //普通的头文件(置入式模型)

    template

    class S {

    public:

            void f();

    };

    template

    void S::f()

    {

    }

    void helper(S*);

    //a.cpp

    #include "t.hpp"

    void help(S* s)

    {

            s->f();   // (1) S::f的第一个POI

    }

    //b.cpp

    #include "t.hpp"

    int main()

    {

            S s;

            help(&s);

            s.f();       //(2)  S::f 的第二个POI

    }

    如果链接器以对待常规函数或常规成员函数方式来处理template的实例化成员,那么编译器便需确保在两个POI处只生成一份程序代码:不是(1)处就是(2)处,但不能两处都生成程序码。为了达到这个目标,编译器不得不把某个编译单元的信息带到其它单元,而这在template概念被引入之前,编译器是绝对无需这么做的。接下来的内容将讨论三种主要解决方案,它们在各种C++编译器实作中运用极广。

        同样的问题也会出现在template实例化过程所产生的所有可链接物linkable entities 上:

    实例化的function template和member function templates以及实例化的static成员变量。

    1. 贪婪式实例化(Greedy Instantiation)

        第一个运用贪婪式机制的编译器出自于Borland公司。后来这种机制逐渐成为各种C++编译器系统中最常用的技术,特别是在微软开发环境中,它几乎成为所有编译系统使用的机制。

        贪婪式实例化机制假设,链接器知道某些物体(特别是可链接之template具现化)可能在各个obj文件和lib文件中存在多份。编译器通常会以特殊方法在这些物体上做出标记。当链接器发现存在多个重复具现体时,它只保留一个,并将其他具现体全部丢弃。就是这么简单。

        贪婪式实例化在理论上有一些严重缺点:

        编译器可能生成并优化N个template具现体,最后却只保留一个。这会严重浪费时间。

        通常链接器并不检查两个具现体是否完全相同,因为要给template特化体的多个实体之间往往存在一些不重要的差异,这是合法的。这些小差异不会导致链接失败。然而这也经常导致链接器注意不到更大的差异,例如某个具现体可能在编译期间针对最大效能进行优化,另一个具现体在编译期间含有更多除错信息。

        和其他机制对比,贪婪式实例化机制可能会产生总长度大得多的obj文件,因为相同的程序可能重复多次。

        这些缺点并不是什么大问题。可能是因为贪婪式机制在一个重要方面明显优于其他机制:传统的源码-目标文件(source-object)依存关系得以保留,特别是:一个编译单元只生成一个obj文件,每个obj文件都包含对应源码文件中的所有可链接定义(其中包括实例化后的定义)。

        最后一个值得注意的问题是,如果链接机制允许多个可链接物(linkable entities)的定义存在,这个机制通常可被用来处理重复出现到处散落的inline函数和虚拟函数分派表virtual function dispatch tables。当然,如果不支持这中机制,也可能使用其他机制以内部链接方式internal linkage 产生这些东西,代价是产生出来的obj文档比较大。

    2. 查询式实例化(Queried Instatiation)

        此类机制最普及的实现来自Sun Microsystem公司,第一个支持此机制的是其编译器4.0版本。查询式实例化概念上及其简单优雅,如果按发生时间排序,它也是我们回顾的这些实例化机制中出现最晚的。该机制中,编译器维护一个由程序各编译单元共享的数据库database。这个数据库可用来追踪“哪些特化体在哪个相关源码文件中被实例化”。生成的所有具现体也会连同这些信息被存储于数据库中。当编译器遇到一个可链接物)(linkable entity)的POI(具现点)时,可能会发生以下三种情况之一:

    1)找不到特化体

        这种情况下,编译器会进行实例化过程,产生的特化体被存入数据库。

    2)找到了特化体,但已经过期out of date

        也就是特化体生成后,源码又被改动过。和1.相同,编译器于是进行实例化过程。然后把新生成的特化体存入数据库中。

    3)在数据库中找到了最新的特化体,编译器什么都不用做。

    虽然概念上简单,但实操时这种设计带来一些难题:

        编译器需要正确的根据源码状态来维护数据库中的各种依存关系,而这并非唾手可得。虽然,把第三种情况误当做第二种情况并不算错误,但这会增加编译器的工作量,也就是增加整个编译时间。

        并行(concurrently)编译多份源码如今已经是颇为平常的事情了。那么编译系统的实作者必须提供一个工业强度的数据库,恰当地控制各种并行情况。

        尽管存在这些困难,这个机制还是可以实现出极高效率的编译器。而且也不存在什么情况会使这种解决办法无法处理大规模程序。相反地,如果采用贪婪式实例化机制,处理大规模程序时会产生许多任务工时的浪费。

        不幸的是,数据库的使用会带给程序员一些问题。多数问题的根源在于,继承自大多数C编译器的传统编译模型,此处不再适用,因为单一编译单元不再产生单一的独立obj文件。举个例子,

    假设你想要链接你的最终版本的程序,链接操作中不仅需要编译单元相应的各个obj文件,也需要存储于数据库中的obj文件。同样道理,如果要建立一个二进制lib文件,必须确保生成该文件的工具程序(通常是linker或archiver)能够感知数据库中的内容。更常见的情况是,所有obj文件操作工具都需要感知数据库的内容。许多这些问题都可以透过不要将具现体保存于数据库中而得以缓和,换之以另一种方式:吐露出obj文件(首先引发实例化)中的obj吗。

        lib文件是另一个大难题。很多编译器生成的特化体可能会被打包到一个lib文件中。当另一个专案使用这个lib时,该项目的数据库必须感知到这些既有的特化体。如果无法感知,编译器就会为此项目产生出它自己的POI。然而这些特化体已经存在于Lib文件中,于是造成重复实例化。

    3. 迭代式实例化(Iterated Instantiation)

        第一个支持C++ template的编译器是Cfront 3.0,Cfront的一个硬性条件是:必须具备高度移植性。因此要求它:

    1)在所有平台上都以C语言为共通表述;

    2)使用平台提供的链接器。

    更确切地说这意味着链接器对template一无所知。事实上Cfront将template具现体生成为常规C函数,因此它必须避免产出重复的具现体。

        尽管Cfront的源码模型不同于标准的置入式模型和分离式模型,但是它的实例化策略可加以改编适应置入式模型。也因此它被人们誉为迭代式实例化机制的第一个化身。

    CFront的迭代式实例化机制描述如下:

    1)编译源码,但不实例化任何所需的可链接特化体(linkable specialization)

    2)使用一个预链接器(prelinker)链接obj文件

    3)预链接器调用链接器,并解析其错误信息以便得知是否遗漏任何具现体。

    4)如果产出任何具现定义,则重复3)

    原始的Cfront方案有一个致命缺点:链接时间大大膨胀了,因为不仅预编译器的额外开销,每次必要的再编译和再链接也需要高额的时间代价。

    下面介绍改良后的一种迭代式实例化机制EDG版本。

    EDG的迭代方式是在预链接器和各编译步骤之间建立双向通信:预链接器可将特定编译单元所需的实例化透过一个实例化申请档instantiation request file转给编译器处理:在obj文件中嵌入某些信息或产生个别template信息文件,编译器有能力告诉预编译器可能的POI位置。实例化申请文件与template信息文件与对应的待编译文件同名,分别带有.ii, 和.ti后缀。

    迭代机制按以下步骤工作:

    1)编译某个编译单元的源码时,EDG编译器读取对应的.ii文件,并产生需要的具现体。同时它将原本可兑现的POI写入当前编译产生的obj文件中,或写入一个独立的.ti文件中。同时也将这个文件如何编译的信息写入。

    2)链接步骤被预编译器拦截。预编译器检查参与链接的obj文件和对应的.ti文件,如果发现有尚未产生的具现体,就把请求写入可兑现此一请求的编译单元所对应的.ii文件中。

    3)如果预链接器察觉到任何.ii文件被更改过。就重新调用编译器来处理对应的源码文件,然后迭代预链接步骤。

    4)当所有具现体都被产出完毕(迭代完成),链接器便会进行实际的链接工作。这个机制解决了并行构建(conrrent build)问题,它把所有用到的信息分置于各编译单元中。

  • 相关阅读:
    传统的回调函数与 ES6中的promise回调以及 ES7 的async/await终极的异步同步化
    json进阶---jackson底层之JsonParser理解使用(springboot多结构参数的映射方法的实现思路)
    (五)共享模型之管程【wait notify 】
    (附源码)spring boot SneakerHome球鞋商城 毕业设计 011229
    OHEM在线难例挖掘原理及在代码中应用
    apt-cache 指令格式以及常用指令
    VP Atcoder Beginner Contest 265
    C#应用程序界面开发基础——窗体控制
    实例解读丨关于GaussDB ETCD服务异常
    java计算机毕业设计教师教学质量评估系统源码+mysql数据库+系统+lw文档+部署 - 副本
  • 原文地址:https://blog.csdn.net/zkmrobot/article/details/126024538