一、介绍
在前面的几篇文章中,都提到了模板的编译问题,为了能够更好地将模板的编译弄清楚,本文将对模板的编译相关问题整体进行分析。主要目的是为了让有志于模板开发的相关开人员能够对模板中具体的相关技术问题有一个从底层的认知。这样,对于模板的开发,会有一个更全面的认知。
二、模板的编译过程
模板的编译过程从整体的流程来看与普通的代码编译没有本质的不同。但这不代表模板编译没有自身的特点。大流程相同,但在细节上却有着不同的处理机制。一般来说,代码的编译分成三个部分:
预编译阶段
这块类似于工厂生产时“材料准备”阶段,将相关的宏、头文件以及条件编译控制以及一些辅助功能如删除注释,处理标记等等
编译阶段
编译阶段是比较复杂的过程,包括词、语法、语义分析优化以及汇编代码生成等等
汇编阶段
这个阶段是关键,用来将前一步骤生成的汇编代码转化为机器代码(二进制的机器指令),然后产生符号信息以及重定位信息等
链接阶段
这个阶段可以算在编译过程中也可以单独拿出来分析(也是一段很复杂的内容),它主要是通过链接器把多个目标文件和相关的库文件链接在一起形成最终可执行的文件
这些内容在书籍和资料上有很多,这里不再详细展开。然后分析一下模板与普通代码编译的不同之处在哪里。模板代码的编译不同主要体现在:
编译阶段的中前期
在这个阶段,普通代码是直接编译,而模板采用的Two-Phase Lookup即两阶段查找机制:
第一阶段即模板定义编译时,编译器会检查基本语法是否符合规范并查找非依赖名称(可以理解为不依赖模板参数的名称)以及对模板代码的初步解析。在这一阶段,是不会进行依赖名称的检查的;第二阶段即模板实例化编译时,则要进行依赖模板参数的代码检查、类型约束的检查以及函数调用的有效性检查。编译器会通过具体类型来替换模板中的参数,并查找依赖的名称,从而最终生成实际的代码。在上述的两个过程中,SFINAE就可以起到控制作用。
编译阶段的汇编代码生成
模板编译因为有一个实例化的过程,与普通代码的直接编译有所不同。
链接阶段
这块的不同在于,模板代码可能会因为实例化引发的代码膨胀产生多个相同的实例,这就需要链接器在链接过程中进行去重。
模板的编译与普通代码的编译细节还有很多,比如template模板的模板参数以及模板嵌套中的依赖限定等等,但这些都不是关键的问题。只要明白其中的道理就可以了。当然在具体的环境中,不同的编译器可能处理模板的编译有所不同,最典型的就是在MSVC上编译通过的模板代码可能在g++上就编译不通过。所以具体的编译情况还需要根据不同的编译器来有针对性的进行处理。
三、模板的CTAD和ADL
在编译阶段中,曾经分析过两个重要的技术内容,CTAD(类模板参数推导)和ADL(参数依赖查找)。前者是C++17引入的,后者则更早。CTAD用来处理类模板构造对象时的模板参数省略的问题,它让编译器自动推导相关的模板参数。比如下面的代码:
template<typename T>
structDemo {
Demo(T t) : v_(t) {}
T v_;
};
Demo d{100}; // CTAD可以推导出模板参数T为int类型
而ADL则是对相关实参的依赖查找,在函数模板中,它非常重要和关键。看下面的代码:
namespace Demo {
structA {};
voidtest(A,int);
}
Demo::A a;
test(a,10); //ADL找到test
它们二者的各有各的应用场景,但在某些情况下可能会共同工作来实现模板的相关参数处理。更详尽的用法可查看前面的相关的文章分析,此处不同详述。
四、具体应用
在模板的编译中,重点要理解模板两阶段编译的过程以及在此过程中对相关的依赖和非依赖名称的处理,其次是要明白CTAD和ADL的应用。下面看一个例子:
#include<initializer_list>
#include<iostream>
#include<vector>
namespace Demo {
structExample {
int v_;
};
voiddisplay(const Example &t){ std::cout << "cur v_ is " << t.v_ << std::endl; }
} // namespace Demo
voiddisplay(int x){ std::cout << "global func x is " << x << std::endl; }
template <typename T> structMyCon {
std::vector<T> vec_; //非依赖
MyCon(std::initializer_list<T> init) : vec_(init) {}
MyCon(const T &t) : vec_{t} {}
voidadd(const T &t){ vec_.push_back(t); }
template <typename U> voidprocess(U &&u){
// 依赖名称
usingstd::begin;
auto it = begin(vec_);
display(u);
}
};
// CTAD
template <typename T> MyCon(std::initializer_list<T>) -> MyCon<T>;
intmain(){
// 使用 CTAD
MyCon con1{3, 6, 9};
MyCon con2{Demo::Example{100}};
// ADL
con1.process(3);
con2.process(Demo::Example{99}); // ADL: Demo::display
return0;
}
五、总结
编译问题是一个相当复杂的问题,但也是一个相当小众的问题。虽然很多人都分析过编译细节甚至相关的原理,但实际上真正能够自己操刀进行编译详细处理的,还是极少的。类似学英语,能看能说的多,真正能写甚至能写得很好的,就很少了。