距离上次写文章已经过去了一年。V2版本已经太监,现在的V3版本终于部分实现了该功能。
鉴于时间比较久远,再把需求赘述一下:
目标:实现可以存储任意函数的容器方案,此处“任意”指函数参数的个数和类型任意。
之前尝试了直接将std::function作为容器元素类型,但是这个方法没法解决函数参数类型不同的问题,所有的容器都要求元素类型一致,要满足这个条件,只能使用std::any类型了。std::function可以存储在std::any中,通过std::any_cast也可以将any变量转换成std::function。
首先,我们定义一个结构体AnyCallable,其最重要函数是 operator()运算符重载函数。
通过可变参数模板来支持任意数量、类型的函数参数调用。
template
调用的方式如下所示,可以绑定函数,类成员函数,或者lambda函数。
void
似乎一切都是那么风和日丽,心想事成,但是我想说的是,有许多坑隐藏在这些简单的调用中。
坑1:
const
调用func_vecs[0]的时候,直接传入"hello"会丢出bad_anycast异常。看起来不可思议,为什么"hello"不能隐式转换成const std::string &?以前不是经常这么用么?让我们来看一下真实的类型。
首先要借助于boost::typeindex::type_id_with_cvr<T>().pretty_name()这个工具(可以输出带cvr修饰的类型信息),看一下在构造时存储在any中的类型,和函数调用时实际传入的参数类型。AnyCallble需要改写成如下样式以增加打印信息。
template
测试1.1:
func_vecs
可以得到打印信息:
std::function<void (std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)>
type list:
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&
测试1.2:
func_vecs[0]("hello");
得到的参数类型如下:
type list:
char const (&) [6]
发现了么?传入的参数类似和类构造时传入的function参数不一致!于是std::any在cast成function的时候就会转换失败,
这是因为std::any_cast并不能理解char const &可以转成const std::string&。
坑2:
当我对函数参数类型做修改,去掉&修饰符,惊奇地发现,调用func_vecs[0](a)失败,丢出bad_any_cast异常。
void
让我们看一下参数类型,可以发现:
// 构造函数的输出
std::function<void (std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)>
// 函数调用时的输出
type list:
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&
神奇么,传入的参数类型变成了 std::string const &, 为什么多了引用符?更奇怪的是下面语句可以正常运行。
func_vecs[0](std::move(a));
以上是我遇到的两个坑。第二个坑等我了解更多关于rvalue和move的信息后再填上吧。(以上代码参见 V3)
补:
终于明白了坑2的原因,和std::bind有关。参考StackOverFlow
通过std::bind绑定的函数大体有两种传参的方式,一种是在初始化时就已经绑定好参数的,另一种是使用占位符std::placeholders::_*来定义的。
- 前者取决于初始化时是否使用referencewrapper<T>,如std::ref或者std::cref。未使用reference_wrap则传入的变量类型就是T,否则就是T &。
- 使用std::placeholders::_*来定义的变量,当函数被调用,如f(v1)时,实际传入的参数为std::forward<V1>(v1),实际传入的类型即为V1&&。这能解释为什么std::move可行。具体的move和forward内容留给下一篇展开来介绍。