查看原文
其他

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

CPP开发者 2022-07-01

The following article is from CPP编程客 Author cpluspluser

1  问题与需求

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

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

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

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

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

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

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

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

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

如何实现这些需求呢?

2  解决方案与实现

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

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

1template<typename T>
2struct function_traits;
3
4template<typename R, typename... Args>
5struct function_traits<std::function<R(Args...)>>
6{

7    static constexpr std::size_t value = sizeof...(Args);
8    using result_type = R;
9
10    template<size_t I>
11    struct get {
12        using type = typename std::tuple_element<I, std::tuple<Args...>>::type;
13    };
14};

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

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

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

来个使用的小例子:

1typedef std::function<void(intdoublestd::string)> FuncType;
2std::cout << mc::function_traits<FuncType>::value << std::endl;
3std::cout << typeid(mc::function_traits<FuncType>::result_type).name() << std::endl;
4std::cout << typeid(mc::function_traits<FuncType>::get<0>::type).name() << std::endl;
5std::cout << typeid(mc::function_traits<FuncType>::get<1>::type).name() << std::endl;
6std::cout << typeid(mc::function_traits<FuncType>::get<2>::type).name() << std::endl;
7
8// Outputs:
9// 3
10// void
11// int
12// double
13// 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对象。

实现如下:

1using PlaceholdersList = std::tuple<decltype(std::placeholders::_1),
2                                    decltype(std::placeholders::_2),
3                                    decltype(std::placeholders::_3),
4                                    decltype(std::placeholders::_4),
5                                    decltype(std::placeholders::_5),
6                                    decltype(std::placeholders::_6),
7                                    decltype(std::placeholders::_7),
8                                    decltype(std::placeholders::_8),
9                                    decltype(std::placeholders::_9),
10                                    decltype(std::placeholders::_10),
11                                    decltype(std::placeholders::_11),
12                                    decltype(std::placeholders::_12),
13                                    decltype(std::placeholders::_13),
14                                    decltype(std::placeholders::_14),
15                                    decltype(std::placeholders::_15),
16                                    decltype(std::placeholders::_16),
17                                    decltype(std::placeholders::_17),
18                                    decltype(std::placeholders::_18),
19                                    decltype(std::placeholders::_19),
20                                    decltype(std::placeholders::_20)>;
21
22template<std::size_t... Is, typename F, typename... Args>
23auto bind_helper(std::index_sequence<Is...>, const F& f, Args&&... args)
24
{
25    return std::bind(f, std::forward<Args>(args)..., 
26        typename std::tuple_element<Is, PlaceholdersList>::type{}...);
27}
28
29
30template<typename FunctionType, typename F, typename... Args>
31auto binder(const F& f, Args&&... args)
32
{
33    return bind_helper(std::make_index_sequence<
34        function_traits<FunctionType>::value>{}, f, std::forward<Args>(args)...);
35}

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

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

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

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

实现如下:

1// the second method
2// user-defined placholder type.
3template<int N>
4struct MyPlaceholder {};
5
6namespace std {
7    template<int N>
8    struct is_placeholder<::MyPlaceholder<N>> : std::integral_constant<int, N> {};
9}

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

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

7
8    // use method 2
9    return std::bind(f, std::forward<Args>(args)..., MyPlaceholder<Is + 1>{}...);
10}

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

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

3  如何使用?借助上述工具优化okdp::subject接口

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

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

1template<typename ConcreteSubject>
2class subject : public ConcreteSubject {
3    // ...
4public:
5
6    Token attach(Target target) {
7        //auto token = std::make_shared<Target>(std::move(callback));
8        std::shared_ptr<Target> token(new Target(std::move(target)), 
9            [&](Target* obj) { delete obj; this->cleanup(); }
10        );
11        observers_.push_back(token);
12        return token;
13    }

编写一段测试代码:

1struct Boss {
2    using ObserverType = std::function<void(const std::string&)>;
3};
4
5void print(const std::string& data) {
6    std::cout << __func__ << data << std::endl;
7}
8
9void update(const std::string& data) {
10    std::cout << __func__ << data << std::endl;
11}
12
13struct Foo {
14    void update(const std::string& obj) {
15        std::cout << "Foo::" << __func__ << obj << std::endl;
16    }
17};
18
19void TestFunc()
20
{
21    okdp::subject<Boss> boss;
22    Foo foo;
23    auto token1 = boss.attach(&print);
24    auto token2 = boss.attach([&foo](const std::string& arg) { return foo.update(arg); });
25    auto token3 = boss.attach(std::bind(&Foo::update, foo, std::placeholders::_1);
26    //auto token4 = boss.attach(&Foo::update, foo);
27}

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

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

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

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

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

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


- EOF -

推荐阅读  点击标题可跳转

1、如何写一个简单的node.js C++扩展

2、C++ std::function技术浅谈

3、谷歌大牛的 C 语言编程建议和技巧


关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子

点赞和在看就是最大的支持❤️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存