1. 模板的魔力发生在编译时。
如下示例中,编译器看到absval
的模板定义,然后看到对absval
函数模板的调用。编译器检查函数参数的类型,并根据函数参数的类型确定模板参数。编译器将该模板参数替换为T
,并生成一个新的absval
函数实例,定制为模板参数类型。
main.cpp
// g++ main.cpp -std=c++14 --save-temps
template<class T>
T absval(T val){
if(val >= 0){
return val;
}
else {
return -val;
}
}
int main() {
int a = absval(4);
float b = absval(-21.f);
double c = absval(100.0);
}
编译生成的汇编代码如下,有以下代码可以看出,每用一个类型调用模板函数在汇编代码里就会有对应的代码生成,因此会有二进制文件膨胀的风险。
.arch armv8-a
.file "main.cpp"
.text
.align 2
.global main
.type main, %function
main:
.LFB1:
.cfi_startproc
stp x29, x30, [sp, -32]!
.cfi_def_cfa_offset 32
.cfi_offset 29, -32
.cfi_offset 30, -24
mov x29, sp
mov w0, 4
bl _Z6absvalIiET_S0_
str w0, [sp, 16]
fmov s0, -2.1e+1
bl _Z6absvalIfET_S0_
str s0, [sp, 20]
mov x0, 4636737291354636288
fmov d0, x0
bl _Z6absvalIdET_S0_
str d0, [sp, 24]
mov w0, 0
ldp x29, x30, [sp], 32
.cfi_restore 30
.cfi_restore 29
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.LFE1:
.size main, .-main
.section .text._Z6absvalIiET_S0_,"axG",@progbits,_Z6absvalIiET_S0_,comdat
.align 2
.weak _Z6absvalIiET_S0_
.type _Z6absvalIiET_S0_, %function
_Z6absvalIiET_S0_:
.LFB2:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str w0, [sp, 12]
ldr w0, [sp, 12]
cmp w0, 0
blt .L4
ldr w0, [sp, 12]
b .L5
.L4:
ldr w0, [sp, 12]
neg w0, w0
.L5:
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.LFE2:
.size _Z6absvalIiET_S0_, .-_Z6absvalIiET_S0_
.section .text._Z6absvalIfET_S0_,"axG",@progbits,_Z6absvalIfET_S0_,comdat
.align 2
.weak _Z6absvalIfET_S0_
.type _Z6absvalIfET_S0_, %function
_Z6absvalIfET_S0_:
.LFB3:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str s0, [sp, 12]
ldr s0, [sp, 12]
fcmpe s0, #0.0
bge .L10
b .L11
.L10:
ldr s0, [sp, 12]
b .L9
.L11:
ldr s0, [sp, 12]
fneg s0, s0
.L9:
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.LFE3:
.size _Z6absvalIfET_S0_, .-_Z6absvalIfET_S0_
.section .text._Z6absvalIdET_S0_,"axG",@progbits,_Z6absvalIdET_S0_,comdat
.align 2
.weak _Z6absvalIdET_S0_
.type _Z6absvalIdET_S0_, %function
_Z6absvalIdET_S0_:
.LFB4:
.cfi_startproc
sub sp, sp, #16
.cfi_def_cfa_offset 16
str d0, [sp, 8]
ldr d0, [sp, 8]
fcmpe d0, #0.0
bge .L16
b .L17
.L16:
ldr d0, [sp, 8]
b .L15
.L17:
ldr d0, [sp, 8]
fneg d0, d0
.L15:
add sp, sp, 16
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.LFE4:
.size _Z6absvalIdET_S0_, .-_Z6absvalIdET_S0_
.ident "GCC: (Ubuntu 13.1.0-8ubuntu1~22.04) 13.1.0"
.section .note.GNU-stack,"",@progbits
编写函数模板比编写普通函数更难。当你编写一个像absval
这样的模板时,问题在于你不知道实际上T
将是什么类型或类型。因此,函数必须是通用的。编译器将阻止你使用某些类型作为T
。它的限制是由模板体对T
的使用方式隐含确定的。
具体来说,absval
对T
施加了以下限制:
T
必须是可复制的。这意味着你必须能够复制类型为T
的对象,这样就可以将参数传递给函数并返回结果。如果T
是一个类类型,那么该类必须有一个可访问的拷贝构造函数,也就是说,拷贝构造函数不能是私有的。T
必须可以使用<
运算符与0
进行比较。你可以重载<
运算符,或者编译器可以将0
转换为T
,或者将T
转换为int
。- 对于类型为
T
的操作数,必须定义一元操作符-
。结果类型必须是T
或编译器可以自动转换为T
的类型。
内置的数字类型都满足这些要求。有理数类型也满足这些要求,因为它支持自定义的运算符。
举个例子,字符串类型不满足这些要求,因为它缺少与整数作为右操作数进行比较的比较运算符,并且缺少一元否定(-
)运算符。假设你尝试在字符串上调用absval
编译会报错。
编译器抱怨在std::string
上缺少比较和否定运算符。在使用模板时提供有用的错误消息的一个困难在于是给出在模板使用的行号还是模板定义的行号。有时,你会同时得到两者。有时,除非你尝试使用模板,否则编译器无法报告模板定义中的错误。其他错误则可以立即报告。
2. 编译器会优先选择非模板函数而不是模板函数
编译器会优先选择非模板函数而不是模板函数,但如果在非模板函数的参数类型和参数类型之间找不到很好的匹配,则会使用模板函数。