目标:实现可以存储任意函数的容器方案,此处“任意”指函数参数的个数和类型任意。
之前尝试了直接将std::function作为容器元素类型,但是这个方法没法解决函数参数类型不同的问题,所有的容器都要求元素类型一致,要满足这个条件,只能使用std::any类型了。std::function可以存储在std::any中,通过std::any_cast也可以将any变量转换成std::function。
首先,我们定义一个结构体AnyCallable,其最重要函数是 operator()运算符重载函数。
通过可变参数模板来支持任意数量、类型的函数参数调用。
template <typename Ret>
struct AnyCallable {
AnyCallable() {}
template <typename... Args>
AnyCallable(std::function<Ret(Args...)> fun) : m_any(fun) {}
template <typename... Args>
Ret operator()(Args &&... args) {
return std::invoke(std::any_cast<std::function<Ret(Args...)>>(m_any),
std::forward<Args>(args)...);
}
std::any m_any;
};
调用的方式如下所示,可以绑定函数,类成员函数,或者lambda函数。
void print_test(const std::string& a)
{
std::cout << a << std::endl;
}
class func_test
{
public:
template<typename value>
void test(value a, value b)
{
std::cout << "a+b=" << a + b << std::endl;
}
};
int main()
{
std::vector<AnyCallable<void> > func_vecs;
func_test ft;
const std::function<void(const std::string &)> &f1 = std::bind(print_test, std::placeholders::_1);
const std::function<void()> &f2 = [](){std::cout << "helloworld." << std::endl;};
const std::function<void(std::string,std::string)> &f3 =
std::bind(&func_test::test<std::string>, &ft, std::placeholders::_1, std::placeholders::_2);
func_vecs.emplace_back(f1);
func_vecs.emplace_back(f2);
func_vecs.emplace_back(f3);
const std::string & a = "hello2";
func_vecs[0](a);
func_vecs[1]();
func_vecs[2](std::string("hello"),std::string("world"));
return 0;
}
似乎一切都是那么风和日丽,心想事成,但是我想说的是,有许多坑隐藏在这些简单的调用中。
坑1:
const std::string & a = "hello2";
func_vecs[0](a); // ok
func_vecs[0]("hello"); // throw exception
调用func_vecs[0]的时候,直接传入"hello"会丢出bad_anycast异常。看起来不可思议,为什么"hello"不能隐式转换成const std::string &?以前不是经常这么用么?让我们来看一下真实的类型。
首先要借助于boost::typeindex::type_id_with_cvr<T>().pretty_name()这个工具(可以输出带cvr修饰的类型信息),看一下在构造时存储在any中的类型,和函数调用时实际传入的参数类型。AnyCallble需要改写成如下样式以增加打印信息。
template <typename Ret>
struct AnyCallable {
AnyCallable() {}
template <typename... Args>
AnyCallable(std::function<Ret(Args...)> fun) : m_any(fun) {
std::cout << boost::typeindex::type_id_with_cvr<std::function<Ret(Args...)>>().pretty_name()<< std::endl;
}
template <typename... Args>
Ret operator()(Args &&... args) {
std::cout << "type list:" << std::endl;
(std::cout << ... << std::string(std::string(boost::typeindex::type_id_with_cvr<Args>().pretty_name()) + "\n"))
<< std::endl;
return std::invoke(std::any_cast<std::function<Ret(Args...)>>(m_any),
std::forward<Args>(args)...);
}
std::any m_any;
};
测试1.1:
func_vecs[0](a);
可以得到打印信息:
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 print_test(const std::string a)
{
std::cout << a << std::endl;
}
// 同样的,函数绑定时也去掉const
const std::function<void(const std::string )> &f1 = std::bind(print_test, std::placeholders::_1);
func_vecs[0](a); // throw exception
让我们看一下参数类型,可以发现:
// 构造函数的输出 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内容留给下一篇展开来介绍。
原作者往期博客: