深入理解函数对象
先给出函数对象的定义一个类对象如果它重载了函数调用运算符 operator()那么这个对象就可以像函数一样被调用这种对象就叫函数对象。#include iostream using namespace std; class MyGreater { public: bool operator()(int a, int b) // 重载了()小括号运算符也叫二元函数对象 { return a b; } }; int main() { MyGreater gt; cout gt(10, 20) endl; // 0 return 0; }可以看出来这个对象可以像函数一样调用但是他本质上等价于gt.operator()(10, 20)那么接下来我们来看一个示例代码template typename T bool mygreater(T a, T b) { return a b; } template typename T bool myless(T a, T b) { return a b; } template typename T, typename Comp bool compare(T a, T b, Comp com) { return com(a, b); } int main() { cout compare(10, 20, mygreaterint) endl; cout compare(10, 20, mylessint) endl; return 0; }这段代码的目的是想让 compare 一个函数同时实现大于小于比较的功能。所以定义了一个compare 的函数模板我们将函数名作为第三个参数传入。这里补充一个 C 的函数指针知识点函数指针的作用就是一个指针变量里面保存的是函数的入口地址。通过这个指针可以间接调用函数。// 这里给出一个简单的函数指针应用示例 int add(int a, int b) // 定义一个 add 函数 { return a b; } int main() { // 下面这句定义了一个函数指针返回值与形参列表类型要一一对应 // 这句话的意思是: pfunc 是一个指针它指向的函数参数是 int, int返回值是 int int (*pfunc)(int, int) add; // 通过函数指针调用函数 int ret pfunc(10, 20); cout ret endl; // 30 return 0; }那么我们回到前面// 在写这两句代码的时候或有一些知识点需要理解 /* 第一: 如果我们在使用函数模板的时候直接使用函数模板名也就是下面代码中的 compare 那么编译器就会根据传入的参数进行自动推导比如下面的 compare就会自动推导出函数名: compareint, bool (*)(int, int) 第二: 函数 compareint, bool (*)(int, int) 中的第三个参数不是写成 bool (*)(int, int) 而是写成 bool (*com)(int, int) 第三: 在模板函数的调用点会根据模板实例化一份代码出来也就类似于如下的代码 bool compareint, bool int (*)(int, int)(int a, int b, int (*com)(int, int)) { com(a, b) } 所以 compareint, bool int (*)(int, int) 叫函数名compare 叫函数模板名 */ cout compare(10, 20, mygreaterint) endl; cout compare(10, 20, mylessint) endl;下面我们来看看使用 C 风格的函数指针的局限性因为编译器实例化出来的函数版本是bool compareint, bool (*)(int, int)(int a, int b, bool (*com)(int, int)) { return com(a, b); }变量 com 里面可以存 mygreaterint也可以存mylessint那么对于上面这个实例化出的 compare 他就是一个通用的版本。我们来看看汇编指令上是如何体现的/* 第一句输出代码的前几句汇编指令: cout compare(10, 20, mygreaterint) endl; 00864CC6 push offset mygreaterint (08613E3h) 00864CCB push 14h 00864CCD push 0Ah 00864CCF call compareint,bool (__cdecl*)(int,int) (08613E8h) */ cout compare(10, 20, mygreaterint) endl; /* 第二句输出代码的前几句汇编指令: 00864D1B push offset mylessint (08613DEh) 00864D20 push 14h 00864D22 push 0Ah 00864D24 call compareint,bool (__cdecl*)(int,int) (08613E8h) */ cout compare(10, 20, mylessint) endl;那么从这里可以看见两次 call 函数调用的地址是一样的我们跳到这个地址上会看见下面的指令008613E8 jmp compareint,bool (__cdecl*)(int,int) (08617B0h)然后再跳到 08617Bh 这个地址就能看见实例化的这个 compare 函数的汇编指令了template typename T, typename Comp bool compare(T a, T b, Comp com) { 008617B0 push ebp 008617B1 mov ebp,esp 008617B3 sub esp,0C4h 008617B9 push ebx 008617BA push esi 008617BB push edi 008617BC lea edi,[ebp-4] 008617BF mov ecx,1 008617C4 mov eax,0CCCCCCCCh 008617C9 rep stos dword ptr es:[edi] 008617CB mov ecx,offset _BBFD6D3E_testcpp (086C07Dh) 008617D0 call __CheckForDebuggerJustMyCode4 (0861334h) 008617D5 nop return com(a, b); // 把形参 com 里面保存的函数地址取出来。 008617D6 mov eax,dword ptr [com] 008617D9 mov dword ptr [ebp-0C4h],eax 008617DF mov esi,esp // 新参压栈 008617E1 mov ecx,dword ptr [b] 008617E4 push ecx 008617E5 mov edx,dword ptr [a] 008617E8 push edx // 重点看这句: 这里并没有写死具体 call 的是哪个函数而是去 [ebp-0C4h] 取出函数地址然后跳过去执行 008617E9 call dword ptr [ebp-0C4h] 008617EF add esp,8 008617F2 cmp esi,esp 008617F4 call __RTC_CheckEsp (086135Ch) } 008617F9 pop edi 008617FA pop esi 008617FB pop ebx 008617FC add esp,0C4h 00861802 cmp ebp,esp 00861804 call __RTC_CheckEsp (086135Ch) 00861809 mov esp,ebp 0086180B pop ebp 0086180C ret所以函数指针调用时调用目标在一个地址变量里如果编译器不知道不明确这个地址运行时指向哪个函数就不能把函数体展开到调用处所以通常无法内联效率很低有函数调用开销那么有没有办法去除这个弊端呢下面我们来看 C 中的函数对象在这方面的表现看下面的代码template typename T class mygreater { public: bool operator()(T a, T b) { return a b; } }; template typename T class myless { public: bool operator()(T a, T b) { return a b; } }; template typename T, typename Comp bool compare(T a, T b, Comp com) { return com(a, b); } int main() { // 这里自动推导类 T: int; Comp: mygreaterint cout compare(10, 20, mygreaterint()) endl; // 这里自动推导类 T: int; Comp: mylessint cout compare(10, 20, mylessint()) endl; return 0; }那么我们来看 main 函数中的两句打印他们会实例化怎样的模板函数出来/* 这里的 compare 实例化出来的模板函数是: bool compareint, mygreaterint(int a, int b, mygreaterint com) { return com(a, b); } */ cout compare(10, 20, mygreaterint()) endl; /* 这里的 compare 实例化出来的模板函数是: bool compareint, mylessint(int a, int b, myglessint com) { return com(a, b); } */ cout compare(10, 20, mylessint()) endl;我们可以看出两次 compare 的函数调用会实例化两个模板函数出来我们到汇编指令上去验证一下/* 009C4CD6 movzx eax,byte ptr [ebp-0C5h] 009C4CDD push eax 009C4CDE push 14h 009C4CE0 push 0Ah 009C4CE2 call compareint,mygreaterint (09C13F7h) */ cout compareint, mygreaterint(10, 20, mygreaterint()) endl; /* 009C4D2E movzx eax,byte ptr [ebp-0D1h] 009C4D35 push eax 009C4D36 push 14h 009C4D38 push 0Ah 009C4D3A call compareint,mylessint (09C13EDh) */ cout compare(10, 20, mylessint()) endl;我们再跳转到地址 09C13F7h 和 09C13EDh 来看一下009C13F7 jmp compareint,mygreaterint (09C17B0h) 009C13ED jmp compareint,mylessint (09C1850h)我们再跳转到 09C17B0h 和 09C1850h 两个地址看一下template typename T, typename Comp bool compare(T a, T b, Comp com) { 009C17B0 push ebp 009C17B1 mov ebp,esp 009C17B3 sub esp,0C0h 009C17B9 push ebx 009C17BA push esi 009C17BB push edi 009C17BC mov edi,ebp 009C17BE xor ecx,ecx 009C17C0 mov eax,0CCCCCCCCh 009C17C5 rep stos dword ptr es:[edi] 009C17C7 mov ecx,offset _BBFD6D3E_testcpp (09CC07Dh) 009C17CC call __CheckForDebuggerJustMyCode4 (09C1334h) 009C17D1 nop return com(a, b); 009C17D2 mov eax,dword ptr [b] 009C17D5 push eax 009C17D6 mov ecx,dword ptr [a] 009C17D9 push ecx 009C17DA lea ecx,[com] //重点看这句: 他没有像之前的函数指针实例化的模板函数一样这里直接 call 了一个函数并且给出了地址 009C17DD call mygreaterint::operator() (09C13F2h) } 009C17E2 pop edi 009C17E3 pop esi 009C17E4 pop ebx 009C17E5 add esp,0C0h 009C17EB cmp ebp,esp 009C17ED call __RTC_CheckEsp (09C135Ch) 009C17F2 mov esp,ebp 009C17F4 pop ebp 009C17F5 ret // ------------------------------------------------------------------------ template typename T, typename Comp bool compare(T a, T b, Comp com) { 009C1850 push ebp 009C1851 mov ebp,esp 009C1853 sub esp,0C0h 009C1859 push ebx 009C185A push esi 009C185B push edi 009C185C mov edi,ebp 009C185E xor ecx,ecx 009C1860 mov eax,0CCCCCCCCh 009C1865 rep stos dword ptr es:[edi] 009C1867 mov ecx,offset _BBFD6D3E_testcpp (09CC07Dh) 009C186C call __CheckForDebuggerJustMyCode4 (09C1334h) 009C1871 nop return com(a, b); 009C1872 mov eax,dword ptr [b] 009C1875 push eax 009C1876 mov ecx,dword ptr [a] 009C1879 push ecx 009C187A lea ecx,[com] // 这里也是一样 009C187D call mylessint::operator() (09C13FCh) } 009C1882 pop edi 009C1883 pop esi 009C1884 pop ebx 009C1885 add esp,0C0h 009C188B cmp ebp,esp 009C188D call __RTC_CheckEsp (09C135Ch) 009C1892 mov esp,ebp 009C1894 pop ebp 009C1895 ret那么根据以上对【函数指针】版本以及【函数对象】版本的分析我们来分析一下为什么【函数对象】版本效率会高呢因为模板的【类型参数】在编译期就得确定好而函数指针中确定 com 的类型是 bool (*)(int, int)但是函数对象中 com 的类型是 mygreater(int) 或者是 myless(int)函数指针: com bool (*)(int, int) 函数对象: com mygreaterint 或 mylessint那么既然在编译器就要i确定 com 是哪个类自然就能确定调用该类的小括号 () 重载函数了mygreaterint(a, b) 或者是 mylessint(a, b)其实这两句就相当于mygreaterint(a, b) com.operator()(a, b) mylessint(a, b) com.operator()(a, b)所以在 call 函数调用的时候就直接调用该重载函数即可。而函数指针他只能确定 com 是个函数指针类型但具体调用哪个函数必须运行的时候查看栈上某个内存中的地址变量再调用该地址下的函数算是一种间接的调用吧。所以再函数对象中他是可以进行内联的所以可以总结如下函数对象com 的类型就是 mygreaterint 或 mylessint调用目标编译期确定容易内联函数指针com 的类型是 bool (*)(int,int)com 里面的地址运行时才知道通常只能间接调用不容易内联但是如果我们不用函数对象能不能做到让函数内联呢答案是可以的我们再看看下面的代码template typename T, bool (*Comp)(T, T) //bool (*Comp)(T, T)这个就是非类型模板参数 bool compare(T a, T b) { return Comp(a, b); } int main() { // 这里不能写成 compare(10, 20)因为他推到不出 Comp ? 所以必须显式赋值 cout compareint, mygreaterint(10, 20) endl; cout compareint, mylessint(10, 20) endl; return 0; }那么编译器会实例化两个不同的模板函数/* bool compareint, mygreaterint(int a, int b) { return Comp(a, b); // 模板里面的非类型参数是可以直接用的 } */ cout compareint, mygreaterint(10, 20) endl; /* bool compareint, mylessint(int a, int b) { return Comp(a, b); } */ cout compareint, mylessint(10, 20) endl;在编译期就能推导出这个 com 指向的是哪个函数把调用目标固定下来了所以不需要间接调用。总结函数指针作为函数参数函数地址是运行时传入的值compare 内部通常只能间接调用不容易内联函数指针作为模板参数函数地址是编译期确定的模板实参每个函数地址生成一个独立 compare 版本调用目标固定容易内联