(1 探讨一)第一个尝试弄清的问题是父类模板与子类模板的模板参数的对应关系,如下图:
我们要弄清的问题是创建 function 对象时,传递的模板参数 _Fty , 传递到其父类 _Func_class 中时 ,父类的模板参数 _Ret 与 _Types 是什么样。这很关键。但模板实例化是发生在编译期间。编译器知道根据其自己定义的语法规则来确定这三个模板参数。所以即使是反汇编调试,也无法跟踪这个确定模板参数的过程。因此只能在模板的成员函数中增加一些打印语句,修改下库代码,如下图:
咱们猜测 , 当 function 的模板参数 _Fty = double ( char , int) 时,其父类模板的模板参数为 _Ret = double , _Types = { char , int }。
即 _Ret 见名知意时函数返回值的类型, _Types 是函数的参数类型。确实是这样的,在验证后。发生这么神奇的一幕,就在于那个承上启下的 宏定义 _Get_function_impl ,其有一个模板参数展开的动作。接着给出测试代码,首先是修改 reset 函数:
编写例子测试一下:
(2 探讨二) 再一个探讨的结论是:虽然创建的 function 对象绑定到某个函数类型。但实际为 function 对象赋值时候,可以采用兼容的函数类型,都可以的。只要实际待执行的函数类型的参数和返回值类型都兼容 function 模板参数的类型。测试如下:
因为数据的隐式类型转换是可以的,存在的。但不合理的类型转换,比如从 int 到 char 的转换,将导致代码行 13 报错,测试如下:
(3 探讨三) 函数的返回值可以从任意类型转换为 void 的类型,符合这个方向的函数,也可以绑定到 function 对象上,测试如下:
但反之则不成立。返回值为 void 的类型,不能转换为别的函数返回值类型。其实任意的函数返回值都不要求的话,就乱套了。测试如下
(4 探讨四) function 对象的内存模型与给 function 对象赋值普通函数或可调用对象时的 function 对象构造过程。结论是 :function 对象占据 64 字节的内存空间。为 function 对象赋值普通函数时 , function 存储该函数的地址。为 function 赋值可调用对象时,则在 function 对象占据的 64 字节内存空间中构造个一模一样的可调用对象。若可调用对象太大( > 48 字节) ,则在堆区构造新的一模一样的可调用对象。
48 的来源是因为 _Func_impl_no_alloc 对象还要包含一个虚函数表指针,这个指针占据 8 个字节。
下图的代码展示了 function 的构造逻辑:
补充:结合上图补充个结论, function( _Fx _Func) ,此 function 的构造函数的形参是值传递,若是给其传递可调用对象,就会有可调用对象的 copy 构造函数被调用 ; 然后在 function 的 64 字节内存空间中构造 _Callee 时,可调用对象的 移动构造函数被调用。也就是说前后创建了三个可调用对象。
我们来测试一下。以上我们的推断思路的关键是确定类模板 _Func_impl_no_alloc 中的模板参数 _Callable 的类型, _Callable 也是即将要构造在 function 内存空间中的类型。 _Callable 可能是函数指针类型或者等同于可调用对象的类型。因此打印一下 _Callable ,修改源代码如下图:
测试例子如下图:
(5 探讨五)即使是小小的可调用对象,也可能直接在堆区 copy 构造。因为判别条件是 _Is_large<_Impl> 这个全局 bool 变量。其定义如下:
咱们修改 头文件的模板的成员函数代码,增加一些打印语句,输出上面的三个测试结果:
测试结果如下:
设置可调用对象的移动构造函数不会抛出异常,再测试一下:
另外这个同样的测试,对于普通函数,会是什么结果呢?根据源码推得 function 对象只是保存了函数的起始地址,将来执行时候根据该地址就可以找到该函数。给出测试结果也支持咱们的这个结论:
(6 探讨六)根据 function 模板的赋值运算符函数的定义,function 对象支持这样的写法的代码(给 function 对象赋值后立马调用):
( function_a = function_b ) (paraA , paraB)
因为源码如图:
举例测试一下:
(7)再补充一些该 function 类的用法,这更接近于实际使用,类似于网站 cppreferrence 的参考性:
(8)
谢谢