设计一个引起舒适的回调接口

这个是我造轮子时的一些探索,涉及到简单的template、lambda、完美转发、SFINAE、policy等一连串的坑,在这里做一下记录。

前言

关于template

首先来点template笑话:如何入门泛型编程?

很简单,你只需知道声明template <typename T>,然后在实例化时用实参替代T就可以了!

那么我们来个实例↓

template<typename T, typename F>
struct callableResult {
  typedef typename std::conditional<
    callableWith<F>::value,
    detail::argResult<false, F>,
    typename std::conditional<
      callableWith<F, T&&>::value,
      detail::argResult<false, F, T&&>,
      typename std::conditional<
        callableWith<F, T&>::value,
        detail::argResult<false, F, T&>,
        typename std::conditional<
          callableWith<F, Try<T>&&>::value,
          detail::argResult<true, F, Try<T>&&>,
          detail::argResult<true, F, Try<T>&>>::type>::type>::type>::type Arg;
  typedef isFuture<typename Arg::Result> ReturnsFuture;
  typedef Future<typename ReturnsFuture::Inner> Return;
};

反正我是挺劝退的,因此在设计的过程中并没有用到复杂的模板,但是多少还是会用点,因为template才是引起舒适的源泉

关于舒适

人与人的XP是不一样的,关于回调怎么设计才算舒适也很有主观性,比如我觉得muduo的回调不太喜欢:

  1. 用到了std::bind,以占位符的形式表示上下文,内部实现第一眼难以读懂
  2. 最外围的类型是std::function,但是实参却不统一,可能是std::function<void()>,也可能是std::function<void(std::string, muduo::Socket)>,签名不够统一

考虑到我肤浅的学识还想到了JavaJava画风的接口可能会定义单一的interface,想要callback就先实现它,比如Android中的onCreate(Bundle),内部实现直接填上对应的Bundle即可,不过也有问题就是这个实现只能是Bundle,想要实现其它类型就必须定义一连串的interface,既不简洁也不自由,可读性我觉得不错,可是只能类实现来做到callback确实太局促了(为了避免不知道怎么取名字还有匿名内部类的做法,但实现过程也是麻烦)

需求

针对前面提到的不足,就是要提需求来改进

  1. 简洁:我不想要callback就必须写一连串的类,用对象语义就好
  2. 统一:函数接口如果都是形如Foo(FooContext),大家都知道上下文该是怎么样的
  3. 实现方便:std::bind用于回调确实有优势,有没有其它替代占位符的思路?

简单的说就是可读性好,实现够短够清晰

还有一些BONUS,比如:

  1. 满足生命周期的安全:回调上下文是可能失效的
  2. 我全都要:可以像重载一样在同一签名实现多种写法,无状态的,还是有上下文的,都要

接下来考虑一步步的实现

Callable封装

首先考虑对函数(调用)的封装

可读性方面我觉得值得参考的一个实现是std::thread

假如有一个函数接口是void func(int a, std::string b),那么std::thread可以这么做

std::thread t { func, 1926, "0817s" };

是不是比下面这些方案要简洁一点?

// 
std::function<void(int, std::string)> f;
Object obj(f);
obj.setContext(1926, "0817s");
// or
Object obj (std::bind(func, 1926, "0817s"));
// or
Object obj { [=] {func(1926, "0817s");} }

怎样做才能对任意可调用对象都像thread的构造函数那样简洁,大概用个template加上std::function封装就可以了,思路就是把所有东西都存到一个void()里,一个能动的实现如下:

class Callable {
public:
    template <typename Func, typename ...Args>
    static Callable make(Func &&functor, Args &&...args) {
        return Callable([=]{functor(args...);}); // 副本必不可少
    }
    void operator()() const { _functor(); }

public:
    Callable() : _functor([]{}) { }

protected:
    using Functor = std::function<void()>;
    Callable(const Functor &functor) : _functor(functor) { }
    Callable(Functor &&functor): _functor(static_cast<Functor&&>(functor)) { }
    Functor _functor;
};

这样可以做到

auto callback = Callable::make(func, 1926, "0817s");
// ...后面任意时机,比如在回调时
callback();

相对完整的demo:

#include <bits/stdc++.h>

class Callable {
public:
    template <typename Func, typename ...Args>
    static Callable make(Func &&functor, Args &&...args) 
        { return Callable([=]{functor(args...);}); }
    void operator()() const { _functor(); }

public:
    Callable() : _functor([]{}) { }
protected:
    using Functor = std::function<void()>;
    Callable(const Functor &functor) : _functor(functor) { }
    Callable(Functor &&functor): _functor(static_cast<Functor&&>(functor)) { }
    Functor _functor;
};

struct Connection {
    template <typename ...Args>
    void onCreate(Args &&...args)
        { createCallback = Callable::make(std::forward<Args>(args)...); }
    template <typename ...Args>
    void onClose(Args &&...args)
        { closeCallback = Callable::make(std::forward<Args>(args)...); }
    void start() {
        std::cout << "connecting..." << std::endl;
        createCallback();
        std::cout << "bye." << std::endl;
        closeCallback();

    }
    Callable createCallback;
    Callable closeCallback;
} conn;

void print(int a, std::string b) {
    std::cout << "[" << a << "::" + b + "]" << std::endl;
}
int main() {
    conn.onCreate(print, 1, "2");
    std::vector<std::string> vec {"jojo","dio"};
    conn.onClose([&] {
        std::cout << "visit number: " << vec.size() << std::endl;
        for(auto &&name : vec) std::cout << name << ' ';
        std::cout << std::endl;
    });
    conn.start();
    return 0;
}

至少我们现在可以:

  1. 任意可调用对象都行,最重要是支持lambda
  2. 调用简洁
  3. 调用类的内部只需存Callable即可

回调注册用宏简化一下就更好了

上下文

上面的只是简单的调用,注册回调方并没有从调用类里拿到什么信息,

而且目前的Callable实现显然是不能占位的,怎样才能做到callback(ctx)这种写法,

很简单,套娃处理

#include <bits/stdc++.h>

class Callable; // 同上

struct ConnectionContext {
    int size;
    std::string name;
};

struct Connection {
    template <typename ...Args>
    void onCreate(Args &&...args)
        { createCallback = Callable::make(std::forward<Args>(args)...); }
    // ++++++++++
    using ContextFunctor = std::function<void(ConnectionContext*)>;
    void onCreateWithCtx(ContextFunctor f)
        { onCreate(std::move(f), &ctx); }
    // ++++++++++
    void start() {
        std::cout << "connecting..." << std::endl;
        createCallback();
    }
    Callable createCallback;
    ConnectionContext ctx { 2, "Blade" };
} conn;

int main() {
    conn.onCreateWithCtx([](ConnectionContext *ctx) {
        std::cout << "name: " << ctx->name << std::endl;
    });
    conn.start();
    return 0;
}

(这里所谓的上下文写的比较水,意会一下)

上下文的统一方式是:需要暴露的接口都放到<Foo>Context这种形式的类中,容易辨认内部实现的细节

调用方式通过套娃的处理后,可以发现,在回调处只需要callback()即可,不需要分别处理它是一个简单的、无状态的调用,还是获取上下文的调用

统一签名

上面的做法中,有一点引起不适的地方在于:函数签名能不能统一一下,都是onCreate可不可以?

重载决议

细节方面不打算过于深究,但是前面提到的疑问踩到了一个重载决议的坑:如果函数签名都是onCreate,那么编译器会选择Args...的函数

可以猜想是重载决议中如果需要转型,那么在重载集当中的匹配等级就会低于用到变长模板参数的情况

那能不能通过避免转型的方式去实现?可以的,需要改两处:

  1. ContextFunctor改为void(*)(ConnectionContext*)
  2. 注册回调方需要使用conn.onCreate(+[](ConnectionContext*){....})

这种+lambda可以在编译时确定为对应的函数指针类型,而不是一个匿名类对象

然而,坑在于需要C++14及以上,其次是+太丑

有没有别的通用方法?

SFINAE

通用方法就是经典方法,我们令Args...的函数版本在调用Context*时替换失败即可

一般都是通过类型萃取堆出一个typename来做编译时检查

不确定标准库是否提供这样的封装,但是手写一个也不是不行,这里用到一些简单的技巧:

  1. decltype确定类型
  2. 尾置返回获得args声明
  3. 指针总是存在nullptr
template<typename F, typename ...Args>
inline constexpr auto isCallable(F &&f, Args &&...args) 
        -> decltype(f(std::forward<Args>(args)...))* {
    return nullptr;
}

// 应用于模板可以
template <typename ...Args, typename = decltype(isCallable<Args...>)> 

为了更加方便就直接提供类型吧

template<typename ...Args>
using IsCallableType = decltype(isCallable<Args...>);

// 应用于模板可以
template <typename ...Args, typename = IsCallableType<Args...>> 

实例:

#include <bits/stdc++.h>

class Callable; // 同上

// +++++++++++
template<typename F, typename ...Args>
inline constexpr auto isCallable(F &&f, Args &&...args) 
        -> decltype(f(std::forward<Args>(args)...))* {
    return nullptr;
}
template<typename ...Args>
using IsCallableType = decltype(isCallable<Args...>);
// +++++++++++

struct ConnectionContext; // 同上

struct Connection {
    // SFINAE
    template <typename ...Args, typename = IsCallableType<Args...>>
    void onCreate(Args &&...args)
        { createCallback = Callable::make(std::forward<Args>(args)...); }
    using ContextFunctor = std::function<void(ConnectionContext*)>;
    // 统一的函数签名
    void onCreate(ContextFunctor f)
        { onCreate(std::move(f), &ctx); }
    void start() {
        std::cout << "connecting..." << std::endl;
        createCallback();
    }
    Callable createCallback;
    ConnectionContext ctx { 2, "Blade" };
} conn;

int main() {
    conn.onCreate([](ConnectionContext *ctx) {
        std::cout << "name: " << ctx->name << std::endl;
    });
    conn.start();
    return 0;
}

生命周期

上下文是可能会失效的,尤其是C++这种生命周期要自己控制的语言

要确保回调上下文的生命周期的安全性,我想到的做法有两种:

  1. 确保回调处本身生命周期远长于调用方
  2. 可以感知上下文是否失效

方法1对于设计上来说需要精打细算,万一算错了呢?

方法2的经典方法是提供weak_ptr,回调时进行提升lock判断即可

这里可能影响到Context的设计,二选一:

  1. Context需要enable_shared_from_this
  2. 回调方直接持有一个std::shared_ptr<Context>

随便哪一种都好吧,我用方法2

#include <bits/stdc++.h>

class Callable;
template<typename F, typename ...Args>
inline constexpr auto isCallable(F &&f, Args &&...args) 
        -> decltype(f(std::forward<Args>(args)...))* {
    return nullptr;
}
template<typename ...Args>
using IsCallableType = decltype(isCallable<Args...>);
struct ConnectionContext {
    int size;
    std::string name;
    // make_shared需要用到构造
    ConnectionContext(int size, std::string name):
        size(size), name(std::move(name)) {}
};

struct Connection {
    template <typename ...Args, typename = IsCallableType<Args...>>
    void onCreate(Args &&...args)
        { createCallback = Callable::make(std::forward<Args>(args)...); }
    using ContextFunctor = std::function<void(ConnectionContext*)>;
    void onCreate(ContextFunctor f)
        { onCreate(std::move(f), ctx.get()); }
    // +++++++++++++++
    using WeakContextFunctor = std::function<void(std::weak_ptr<ConnectionContext>)>;
    void onCreate(WeakContextFunctor f)
        { onCreate(std::move(f), ctx); }
    // +++++++++++++++
    void start() {
        std::cout << "connecting..." << std::endl;
        createCallback();
    }
    Callable createCallback;
    // Context受到影响
    std::shared_ptr<ConnectionContext> ctx {std::make_shared<ConnectionContext>(2, "Blade")};
} conn;

int main() {
    conn.onCreate([](std::weak_ptr<ConnectionContext> context) {
        // 提升
        if(auto ctx = context.lock()) {
            std::cout << "name: " << ctx->name << std::endl;
        }
    });
    conn.start();
    return 0;
}

宏简化

写一大串机械的template和声明对于内部实现非常冗余,

我们使用macro来简化流程,废话只说一遍就够了

// 前略

#define CALLBACK_DEFINE(functor, callback, ContextType, context) \
    template <typename ...Args, typename = IsCallableType<Args...>> \
    void functor(Args &&...args) \
        { callback = Callable::make(std::forward<Args>(args)...); } \
    void functor(std::function<void(ContextType*)> f) \
        { functor(std::move(f), context.get()); } \
    void functor(std::function<void(std::weak_ptr<ContextType>)> f) \
        { functor(std::move(f), context); }

struct Connection {

    // +++++++++++++++
    CALLBACK_DEFINE(onCreate, createCallback, ConnectionContext, ctx)
    CALLBACK_DEFINE(onClose, closeCallback, ConnectionContext, ctx)
    // +++++++++++++++

    void start() {
        std::cout << "connecting..." << std::endl;
        createCallback();
        std::cout << "bye." << std::endl;
        closeCallback();
    }
    Callable createCallback;
    Callable closeCallback;
    std::shared_ptr<ConnectionContext> ctx {std::make_shared<ConnectionContext>(2, "Blade")};
} conn;

int main() {
    conn.onCreate([](std::weak_ptr<ConnectionContext> context) {
        if(auto ctx = context.lock()) {
            std::cout << "name: " << ctx->name << std::endl;
        }
    });
    conn.onClose([] { std::cout << "See you again." << std::endl; });
    conn.start();
    return 0;
}

一个宏就足够定义三种统一签名的写法,Connection内部实现也简洁了很多

为了宏也显得简洁,这里实用by-value加上std::move来少写两个左值右值版本的实现

可不可以接着再省一点?用到前面SFINAE是可以的

#define CALLBACK_DEFINE(functor, callback, ContextType, context) \
    template <typename ...Args, typename = IsCallableType<Args...>> \
    void functor(Args &&...args) \
        { callback = Callable::make(std::forward<Args>(args)...); } \
    template <typename Lambda, typename U = IsCallableType<Lambda, ContextType*>> \
    void functor(Lambda &&f, U** = nullptr) \
        { functor(std::forward<Lambda>(f), context.get()); } \
    template <typename Lambda, typename U = \
        IsCallableType<Lambda, std::weak_ptr<ContextType>>> \
    void functor(Lambda &&f, U*** = nullptr) \
        { functor(std::forward<Lambda>(f), context); }

这里直接将lambda完美转发到functor中,省了一步转型到std::function的开销,并且蹭了引用折叠的好处实现了左值和右值的转发

U**=nullptr这个是避免重载决议的坑,因为第二个和第三个可能同时满足导致签名冲突,似乎不够完美,是个可以改进的点

PS.用U*会死得很惨,原因很好推测

占位符

这个是造轮子造到一半踩到的坑,前面提到的套娃设计的前提在于:我持有了context

万一没有呢?比如这样

struct Server {
    // 1. onCreate是派发到每一个connection中
    template <typename ...Args, typename = IsCallableType<Args...>>
    void onCreate(Args &&...args) {
        createCallback = Callable::make(std::forward<Args>(args)...);
    }
    // 2. 企图用Callable封装存储一个有上下文参数的lambda
    template <typename Lambda, typename = IsCallableType<Lambda, ConnectionContext*>>
    void onCreate(Lambda &&f) {
        // createCallback = ...?
    }
    // ....各种回调注册
    void start() {
        Connection conn;
        conn.onCreate(createCallback);
        conn.onClose(closeCallback);
        conns.emplace_back(std::move(conn));
    }
    // 3. 这里中转用到的是Callable,显然无法存下去
    Callable createCallback;
    Callable closeCallback;
    std::vector<Connection> conns;
} server;

int main() {
    server.onCreate([](ConnectionContext*){});
    server.start();
}

是不是要用std::bind来占个坑?

头铁不爱用可不可以有别的方法?

通用类型

std::any可以存下任意copyable的类型,可以通过any_cast<>来安全地转型

它的定位类似于JavaObject,但不是通过继承,而是template加上多态,这里不具体描述实现

假如我们的callback类型是std::any,那么第二部是不是可以随便赋值了?

struct Server {
    template <typename ...Args, typename = IsCallableType<Args...>>
    void onCreate(Args &&...args) {
        createCallback = Callable::make(std::forward<Args>(args)...);
    }
    template <typename Lambda, typename = IsCallableType<Lambda, ConnectionContext*>>
    void onCreate(Lambda &&f) {
        createCallback = std::forward<Lambda>(f);
    }
    void start() {
        Connection conn;
        // ?
        conns.emplace_back(std::move(conn));
    }
    std::any createCallback;
    std::any closeCallback;
    std::vector<Connection> conns;
} server;

可是问题又转嫁到start()里了,我怎么知道any里面是什么类型

用个额外的变量来维护吧

struct Server {
    template <typename ...Args, typename = IsCallableType<Args...>>
    void onCreate(Args &&...args) {
        createCallback = Callable::make(std::forward<Args>(args)...);
    }
    template <typename Lambda, typename = IsCallableType<Lambda, ConnectionContext*>>
    void onCreate(Lambda &&f) {
        createCallback = std::forward<Lambda>(f);
        createState = CTX;
    }
    void start() {
        Connection conn;
        // WTF...
        if(createState == ARGS) {
            conn.onCreate(any_cast<Callable>(createCallback));
        } else if(createState == CTX) {
            conn.onCreate(any_cast</***?***/>(createCallback));
        } else {
            // ...
        }
        // ....
        conns.emplace_back(std::move(conn));
    }
    //
    enum State { ARGS, CTX, WEAK_CTX } ;
    State createState;
    std::any createCallback;
    std::any closeCallback;
    std::vector<Connection> conns;
} server;

然后你的代码结构就变成一坨了,不仅要额外的state记录,start时还要枚举state,最惨的是Lambda类型拿不出手,不得不退回到原来的std::function声明

Policy

前面是非常失败的设计,但是可以从难受的if-else中获得启发,

最擅长做这种事情的一种设计模式是什么?策略模式

还有就是我为什么这么执着于多次onCreate枚举?类型信息的丢失

那么template套上策略模式是不是可以解决?答案是yes

尝试在Server类中新增这些内部成员

struct Policy {
    virtual void onCreate(Connection*) = 0;
    virtual void onClose(Connection*) = 0;
    virtual ~Policy() { }
}; 
std::unique_ptr<Policy> createPolicy;
std::unique_ptr<Policy> closePolicy;
template <typename TypeInfo>
struct PolicyImpl: public Policy {
    TypeInfo runtimeInfo;
    PolicyImpl(TypeInfo &&info)
        : runtimeInfo(std::forward<TypeInfo>(info)) { }
    void onCreate(Connection *conn) override
        { conn->onCreate(runtimeInfo); }
    void onClose(Connection *conn) override
        { conn->onClose(runtimeInfo); }
};

这里用到泛型擦除,使得createPolicy可以存下不同callback类型的runtimeInfo

并且用到策略模式,让类型的选择由ctx->onCreate(TypeInfo)编译时决定

实例如下

#include <bits/stdc++.h>

class Callable {
public:
    template <typename Func, typename ...Args>
    static Callable make(Func &&functor, Args &&...args) 
        { return Callable([=]{functor(args...);}); }
    void operator()() const { _functor(); }

public:
    Callable() : _functor([]{}) { }
protected:
    using Functor = std::function<void()>;
    Callable(const Functor &functor) : _functor(functor) { }
    Callable(Functor &&functor): _functor(static_cast<Functor&&>(functor)) { }
    Functor _functor;
};

template<typename F, typename ...Args>
inline constexpr auto isCallable(F &&f, Args &&...args) 
        -> decltype(f(std::forward<Args>(args)...))* {
    return nullptr;
}
template<typename ...Args>
using IsCallableType = decltype(isCallable<Args...>);

struct ConnectionContext {
    int size;
    std::string name;
    ConnectionContext(int size, std::string name):
        size(size), name(std::move(name)) {}
};

#define CALLBACK_DEFINE(functor, callback, ContextType, context) \
    template <typename ...Args, typename = IsCallableType<Args...>> \
    void functor(Args &&...args) \
        { callback = Callable::make(std::forward<Args>(args)...); } \
    template <typename Lambda, typename U = IsCallableType<Lambda, ContextType*>> \
    void functor(Lambda &&f, U** = nullptr) \
        { functor(std::forward<Lambda>(f), context.get()); } \
    template <typename Lambda, typename U = \
        IsCallableType<Lambda, std::weak_ptr<ContextType>>> \
    void functor(Lambda &&f, U*** = nullptr) \
        { functor(std::forward<Lambda>(f), context); }

struct Connection {

    CALLBACK_DEFINE(onCreate, createCallback, ConnectionContext, ctx)
    CALLBACK_DEFINE(onClose, closeCallback, ConnectionContext, ctx)

    void start() {
        std::cout << "connecting..." << std::endl;
        createCallback();
        std::cout << "bye." << std::endl;
        closeCallback();
    }

    Callable createCallback;
    Callable closeCallback;
    std::shared_ptr<ConnectionContext> ctx 
        {std::make_shared<ConnectionContext>(2, "Blade")};
} conn;

// ++++++++++++++++++++++++++++++

#define POLICY_CALLBACK_DEFINE(callback, policy) \
    template <typename ...Args, typename = IsCallableType<Args...>> \
    void callback(Args &&...args) { \
        policy = std::make_unique<PolicyImpl<Callable>>( \
            Callable::make(std::forward<Args>(args)...)); \
    } \
    template <typename Lambda> \
    void callback(Lambda &&f) { \
        policy = std::make_unique<PolicyImpl<Lambda>>(std::forward<Lambda>(f)); \
    } 
// ++++++++++++++++++++++++++++++

struct Server {
    // ++++++++++++++++++++++++++++++
    POLICY_CALLBACK_DEFINE(onCreate, createPolicy)
    POLICY_CALLBACK_DEFINE(onClose, closePolicy)
    // ++++++++++++++++++++++++++++++
    void startInit(int count) {
        for(; count-- > 0;) {
            Connection conn;
            if(createPolicy) createPolicy->onCreate(&conn);
            if(closePolicy) closePolicy->onClose(&conn);
            conns.emplace_back(std::move(conn));
        }

    }
    void startConn() {
        for(auto &&conn : conns) conn.start();
    }
    // ++++++++++++++++++++++++++++++
    struct Policy {
        virtual void onCreate(Connection*) = 0;
        virtual void onClose(Connection*) = 0;
        virtual ~Policy() { }
    }; 
    template <typename TypeInfo>
    struct PolicyImpl: public Policy {
        TypeInfo runtimeInfo;
        PolicyImpl(TypeInfo &&info)
            : runtimeInfo(std::forward<TypeInfo>(info)) { }
        void onCreate(Connection *conn) override
            { conn->onCreate(runtimeInfo); }
        void onClose(Connection *conn) override
            { conn->onClose(runtimeInfo); }
    };
    std::unique_ptr<Policy> createPolicy;
    std::unique_ptr<Policy> closePolicy;
    // ++++++++++++++++++++++++++++++

    std::vector<Connection> conns;

} server;

void createMsg(const char *str) {
    std::cout << str << std::endl;
} 
int main() {
    server.onCreate(createMsg, "onCreate");
    server.onClose([](std::weak_ptr<ConnectionContext> context) {
        if(auto ctx = context.lock()) {
            std::cout << "onClose: " << ctx->name << std::endl; 
        }
    });
    server.startInit(2);
    auto changeCloseFunc = [](ConnectionContext *ctx) {
        std::cout << "onClose: " << ctx->size << std::endl; 
    };
    server.onClose(changeCloseFunc);
    server.startInit(1);
    server.startConn();
    return 0;
}

发表评论

邮箱地址不会被公开。 必填项已用*标注