关于std::function,几个行之有效的扩展小技巧

开发中,若你的项目稍微具有点扩展性和灵活性,那便少不了会用到std::function。

std::function可以容纳任何形式的可调用体,比如普通函数,成员函数,Lambda 函数。

因此,可以借其来实现两个重要的功能:接口分离和时间分离。

接口分离指的是调用者和被调用者之间彼此分离,以降低二者的依存性。具体来说,你可以将任何可调用体保存到std::function中,可调用体不知道std::function的存在,反之亦如此。于是,可以做什么呢?将具体的处理方式等到用的时候再进行指定,调用者通过std::function这个桥梁,以这个随后指定的方式来处理实际的工作。

那时间分离有什么用呢?普通的函数,当你得到了实际数据,便可通过参数进行调用,也就是说得到数据的同时也就满足了调用的条件。在这里,函数的调用和满足的条件是紧密相连的。时间分离就是将这两部分分离,把调用的函数先保存起来,待条件满足之时再进行调用。

总而言之,通过接口分离和时间分离,可以降低模块之间的耦合,使程序更加具有可扩展性,使用起来会更加灵活。

正因如此,在过去的许多文章中,我们多次使用std::function来完成库的某些功能设计。

然而,有时你需要知道待调用函数的数据个数,也就是参数个数,甚至具体某位参数的类型。

亦或是,当你使用std::bind产生一个可调用体的时候,想要隐藏那些烦人的placeholders。

如何实现这些需求呢?

我们可以通过TMP来扩展std::function,添加一些小巧轻便的辅助工具,来实现上述需求。

建立一个function_traits模板类,用于萃取需要从std::function中获取的信息,代码如下:

 template<typename T>
 struct function_traits;
 
 template<typename R, typename... Args>
 struct function_traits<std::function<R(Args...)>>
 {
    static constexpr std::size_t value = sizeof...(Args);
    using result_type = R;
 
    template<size_t I>
    struct get {
       using type = typename std::tuple_element<I, std::tuple<Args...>>::type;
   };
};

其中,通过sizeof...得到std::function中参数包的大小,存到value中。

对于参数包中的每个具体类型,如何操作呢?

我们讲过,对于类型操作的强大工具是TypeList,而std::tuple就是标准中TypeList的实现。所以借助std::tuple提供的索引式访问,想要获取具体某位的参数类型也不是什么难事。

来个使用的小例子:

typedef std::function<void(int, double, std::string)> FuncType;
 std::cout << mc::function_traits<FuncType>::value << std::endl;
 std::cout << typeid(mc::function_traits<FuncType>::result_type).name() << std::endl;
 std::cout << typeid(mc::function_traits<FuncType>::get<0>::type).name() << std::endl;
 std::cout << typeid(mc::function_traits<FuncType>::get<1>::type).name() << std::endl;
 std::cout << typeid(mc::function_traits<FuncType>::get<2>::type).name() << std::endl;
 
 // Outputs:
 // 3
// void
// int
// double
// class std::basic_string<char, struct std::char_traits<char>, class std::allocator<char> 

 

解决了std::function类型信息的问题,接着来看如何对std::bind的调用进行简化。

简化std::bind的核心问题在于如何自动填充placeholders,总的来说,有两种方法。

第一种是将所有placeholders的类型保存到std::tuple中,那么经由索引式访问,我们就可以得到具体某位的placeholder对象。

实现如下:

using PlaceholdersList = std::tuple<decltype(std::placeholders::_1),
 decltype(std::placeholders::_2),
                                   decltype(std::placeholders::_3),
                                    decltype(std::placeholders::_4),
                                   decltype(std::placeholders::_5),
                                   decltype(std::placeholders::_6),
                                  decltype(std::placeholders::_7),
                                 decltype(std::placeholders::_8),
                                decltype(std::placeholders::_9),
                                  decltype(std::placeholders::_10),
                                    decltype(std::placeholders::_11),
                                   decltype(std::placeholders::_12),
                                    decltype(std::placeholders::_13),
                                  decltype(std::placeholders::_14),
                                  decltype(std::placeholders::_15),
                                 decltype(std::placeholders::_16),
                                  decltype(std::placeholders::_17),
                                  decltype(std::placeholders::_18),
                                  decltype(std::placeholders::_19),
                                  decltype(std::placeholders::_20)>;
template<std::size_t... Is, typename F, typename... Args>
auto bind_helper(std::index_sequence<Is...>, const F& f, Args&&... args)
{
   return std::bind(f, std::forward<Args>(args)..., 
       typename std::tuple_element<Is, PlaceholdersList>::type{}...);
}


template<typename FunctionType, typename F, typename... Args>
auto binder(const F& f, Args&&... args)
{
    return bind_helper(std::make_index_sequence<
        function_traits<FunctionType>::value>{}, f, std::forward<Args>(args)...);
}

在这里就用到了前面实现的function_traits来获取具体的参数个数,再借助std::index_sequece,便可得到所需填充类型的索引。

知道了索引,也就可从PlaceholdersList得到具体的对象。

第二种方案是自定义placeholders,思路可以参考cppreference。

因为std::bind是依赖std::is_placeholder来判断一个类型是否是placeholder,所以可以通过特化std::is_placeholder来定义自己的placeholder类型。

实现如下:

// the second method
// user-defined placholder type.
template<int N>
struct MyPlaceholder {};

namespace std {
   template<int N>
  struct is_placeholder<::MyPlaceholder<N>> : std::integral_constant<int, N> {};
}

现在,需要在bind_helper中使用它来替换第一种方案:

 template<std::size_t... Is, typename F, typename... Args>
 auto bind_helper(std::index_sequence<Is...>, const F& f, Args&&... args)
 {
     // use method 1
     /*return std::bind(f, std::forward<Args>(args)..., 
        typename std::tuple_element<Is, PlaceholdersList>::type{}...);*/
 
     // use method 2
    return std::bind(f, std::forward<Args>(args)..., MyPlaceholder<Is + 1>{}...);
}

显而易见,第二种方式要更加简洁,代码量更少,所以推荐使用这种方式。

该工具被我放在了mcevil库中,位于命名空间mc之下,具体代码可见:https://github.com/lkimuk/mcveil/blob/main/function_traits.hpp。

来看看如何使用上述工具,来优化上节实现的泛型观察者okdp::subject接口。

再来回顾下其中的接口,具体可见文末的「相关文章」:

template<typename ConcreteSubject>
 class subject : public ConcreteSubject {
     // ...
 public:
 
    Token attach(Target target) {
        //auto token = std::make_shared<Target>(std::move(callback));
        std::shared_ptr<Target> token(new Target(std::move(target)), 
           [&](Target* obj) { delete obj; this->cleanup(); }
      );
        observers_.push_back(token);
       return token;
    }

 

 

编写一段测试代码:

struct Boss {
     using ObserverType = std::function<void(const std::string&)>;
 };
 
 void print(const std::string& data) {
   std::cout << __func__ << data << std::endl;
 }
 
 void update(const std::string& data) {
    std::cout << __func__ << data << std::endl;
}

struct Foo {
    void update(const std::string& obj) {
        std::cout << "Foo::" << __func__ << obj << std::endl;
    }
};

void TestFunc()
{
    okdp::subject<Boss> boss;
    Foo foo;
    auto token1 = boss.attach(&print);
    auto token2 = boss.attach([&foo](const std::string& arg) { return foo.update(arg); });
   auto token3 = boss.attach(std::bind(&Foo::update, foo, std::placeholders::_1);
    //auto token4 = boss.attach(&Foo::update, foo);
}

由于接口的定义形式,可以通过三种方式来进行调用,分别是直接传函数地址,Lambda形式和通过std::bind的形式。

但不论使用Lambda还是std::bind,调用之时要写的代码都很长,随着参数的增多,会极为不便。

此时,就可以借助上述实现的mc::binder来进行简化调用。

为okdp::subject添加一个重载版本的attach函数:

template<typename F, typename T, typename... Args>
Token attach(const F& f, T&& head, Args&&... args) {
   return attach(mc::binder<Target>(f, std::forward<T>(head), std::forward<Args>(args)...));
}

通过两个重载版本,例子中的四种调用方式就都可以支持,这将极大的提高程序的灵活性和易用性。

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值