C++11 模板元编程 - 类型校验
一般情况下一个系统可以发送和接收的消息是确定的。例如前面的例子中,visitor可以发送AccessReq
消息,可以接收AccessRsp
消息,然而客户在描述测试用例时却可以传递非法的lambda给visitor。
例如:
TEST(...)
{
visitor.send([this](CfgReq& req)
{
req.capability = CAPABILITY;
});
//...
}
此时,visitor将会构造错误的消息发送给SUT,这可能会引起测试失败,也可能不会。但无论如何,这种错误都会引起运行期发生一些异常。作为一个测试框架,我们希望能够让用户抉择是否把这种错误的发现提前到编译期,一旦不小心传递了某一FakeSystem不支持的消息类型,就直接编译失败。同时用户也可能想保留不进行类型校验的权利,这样就允许用户刻意构造一些错误类型消息以触发异常流程的测试。
FakeSystem
既可以选择校验消息类型,也可以选择不校验,而且这种权利在客户手里,于是我们采用基于policy的设计。对于模板所谓基于policy的设计,就是将类的变化部分分离出去,交给一个模板参数,然后再将其组合进来。而用户就可以通过改变模板参数来定制目标类型的可变化部分了。
如下,我们首先定义一个FakeSystemBase
,它和FakeSystem
代码基本一样,只是它将如何进行消息检测的规则通过模板参数MsgChecker
注入进来,并调用MsgChecker
对消息类型进行静态校验。
template<typename MsgChecker>
struct FakeSystemBase
{
template<typename BUILDER>
void send(const BUILDER& builder)
{
using Msg = __lambda_para(BUILDER, 0);
ASSERT_TRUE(typename MsgChecker::template IsValidForSend<Msg>);
MsgAllocator<Msg> allocator;
auto msg = allocator.alloc();
builder(*msg);
// ...
}
template<typename Checker>
void recv(const Checker& checker)
{
using Msg = __lambda_para(Checker, 0);
ASSERT_TRUE(typename MsgChecker::template IsValidForRecv<Msg>);
// ...
}
};
如上代码约束MsgChecker
的内部必须定义两个模板元函数IsValidForSend<Msg>
和IsValidForRecv<Msg>
,分别对用户传入的发送和接收的消息类型进行合法性校验。上面的ASSERT_TRUE(typename MsgChecker::template IsValidForSend<Msg>)
中调用了TLP测试框架中的断言ASSERT_TRUE
,它是静态断言,当判断类型不符就会编译失败。
接下来我们首先来实现一个MsgChecker
,在任何情况下都返回真,以便实现不用校验的FakeSystem
。
struct OmmitMsgChecker
{
template<typename Msg>
using IsValidForSend = __true();
template<typename Msg>
using IsValidForRecv = __true();
};
有了它,原来的FakeSystem
的定义修改如下。我们消除了重复代码,并且保证了FakeSystem
原有的使用习惯不变。
struct FakeSystem : FakeSystemBase<OmmitMsgChecker>
{
};
接来下我们定义对消息严格校验的StrictFakeSystem
。如下,它需要将发送和接收的合法消息列表当做模板参数传入,以供类型校验使用。
struct OmmitVerify;
template< typename ValidSendMsgs = OmmitVerify
, typename ValidRecvMsgs = OmmitVerify>
struct StrictFakeSystem : FakeSystemBase<StrictFakeSystem<ValidSendMsgs, ValidRecvMsgs>>
{
template<typename Msg>
using IsValidForSend = __if(__is_eq(ValidSendMsgs, OmmitVerify), __true(), __is_included(ValidSendMsgs, Msg));
template<typename Msg>
using IsValidForRecv = __if(__is_eq(ValidRecvMsgs, OmmitVerify), __true(), __is_included(ValidRecvMsgs, Msg));
};
StrictFakeSystem
使用时需要传入模板参数ValidSendMsgs
和ValidRecvMsgs
,它们是两个TypeList,分别代表合法的发送消息类型列表和接收消息类型列表。它们还具有默认参数OmmitVerify
,一旦使用默认参数,则代表放弃对发送或者接收消息的校验能力。StrictFakeSystem
的实现采用之前介绍过的CRTP(Curiously Recurring Template Pattern)
模式。
现在客户可以选择将某些FakeSystem定义成需要严格判断消息类型的了,例如:
using FakeVisitor = StrictFakeSystem<__type_list(AccessReq, CapabilityUpdate), __type_list(AccessRsp, UpdateRsp)>;
FakeVisitor visitor;
上述客户声明FakeVisitor
仅支持发送AccessReq
和CapabilityUpdate
消息,仅支持接收AccessRsp
和UpdateRsp
消息。一旦用户传入的lambda表达式中的消息类型不符,就会出现编译错误,如:visitor.send([](CfgReq& req){...})
将会触发由静态断言失败引起的编译错误。
最后有了StrictFakeSystem
,原有的FakeSystem
的定义也可以修改为:
struct FakeSystem : StrictFakeSystem<>
{
};
至此,我们保留了原有FakeSystem
的用法,同时又让用户可以定义严格校验发送和接收的消息类型的fake系统,从而让框架变得更灵活和安全。
关于模板元编程在dates中的应用就介绍到这里。我们通过实际的例子列举了模板元编程在现实代码中的常用场景和使用技巧:
-
对trait的合理应用,可以让代码更加灵活,让用户代码更加简洁;
-
基于policy的设计技巧,可以让用户自定义目标类型的变化部分,可以让代码更易于被复用和组合;
-
通过类型抉择选择合适的算法类,让代码面对不同场景自动选择最佳的处理方式,同时这种选择又是对客户透明的;
-
通过类型校验,让错误尽早发生,让代码更加的安全和健壮;
接下来,我们将介绍模板元编程更高阶的用法,用于做代码生成和创建DSL语言。