C++函数调用解耦

目标:实现可以存储任意函数的容器方案,此处“任意”指函数参数的个数和类型任意。

之前尝试了直接将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内容留给下一篇展开来介绍。

原作者往期博客:

C++函数调用解耦-V1 - 知乎

C++函数调用解耦-V2 - 知乎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值