这个是我造轮子时的一些探索,涉及到简单的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
的回调不太喜欢:
- 用到了
std::bind
,以占位符的形式表示上下文,内部实现第一眼难以读懂 - 最外围的类型是
std::function
,但是实参却不统一,可能是std::function<void()>
,也可能是std::function<void(std::string, muduo::Socket)>
,签名不够统一
考虑到我肤浅的学识还想到了Java
,Java
画风的接口可能会定义单一的interface
,想要callback就先实现它,比如Android
中的onCreate(Bundle)
,内部实现直接填上对应的Bundle
即可,不过也有问题就是这个实现只能是Bundle
,想要实现其它类型就必须定义一连串的interface
,既不简洁也不自由,可读性我觉得不错,可是只能类实现来做到callback确实太局促了(为了避免不知道怎么取名字还有匿名内部类的做法,但实现过程也是麻烦)
需求
针对前面提到的不足,就是要提需求来改进
- 简洁:我不想要
callback
就必须写一连串的类,用对象语义就好 - 统一:函数接口如果都是形如
Foo(FooContext)
,大家都知道上下文该是怎么样的 - 实现方便:
std::bind
用于回调确实有优势,有没有其它替代占位符的思路?
简单的说就是可读性好,实现够短够清晰
还有一些BONUS,比如:
- 满足生命周期的安全:回调上下文是可能失效的
- 我全都要:可以像重载一样在同一签名实现多种写法,无状态的,还是有上下文的,都要
接下来考虑一步步的实现
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;
}
至少我们现在可以:
- 任意可调用对象都行,最重要是支持lambda
- 调用简洁
- 调用类的内部只需存
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...
的函数
可以猜想是重载决议中如果需要转型,那么在重载集当中的匹配等级就会低于用到变长模板参数的情况
那能不能通过避免转型的方式去实现?可以的,需要改两处:
ContextFunctor
改为void(*)(ConnectionContext*)
- 注册回调方需要使用
conn.onCreate(+[](ConnectionContext*){....})
这种+lambda
可以在编译时确定为对应的函数指针类型,而不是一个匿名类对象
然而,坑在于需要C++14及以上,其次是+
太丑
有没有别的通用方法?
SFINAE
通用方法就是经典方法,我们令Args...
的函数版本在调用Context*
时替换失败即可
一般都是通过类型萃取堆出一个typename
来做编译时检查
不确定标准库是否提供这样的封装,但是手写一个也不是不行,这里用到一些简单的技巧:
decltype
确定类型- 尾置返回获得
args
声明 - 指针总是存在
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的经典方法是提供weak_ptr
,回调时进行提升lock
判断即可
这里可能影响到Context
的设计,二选一:
Context
需要enable_shared_from_this
- 回调方直接持有一个
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<>
来安全地转型
它的定位类似于Java
的Object
,但不是通过继承,而是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;
}