十一过后, 公司的项目迎来了一轮大规模的重构. 过去事件模组作为较为核心的功能, 大量使用了范型来增强通用性, 试图将劳动力从无休止的重复劳动中解放出来. 但事件模块也有一些不够好的地方, 例如在发布事件时经常需要将枚举类型强制类型转换为带有各种修饰符的整形数据, 并需要在回调函数中以整形的形式接收再转换为枚举类型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum class Type : uint8_t {
CASE1,
CASE2,
CASE3,
};

event::Regist<TASK1> cb(event::ID, +[](const uint8_t type) {
switch ((Type)type) {
case Type::CASE1:
/* ...do something... */
break;
case Type::CASE2:
/* ...do something... */
break;
case Type::CASE3:
/* ...do something... */
break;
default:
break;
}
});

void func() {

/* ...do something... */

event::notify<TASK1>(event::ID::EVT1, (uint8_t)Type::CASE1);

/* ...do something... */
}


这种冗余操作的本质是因为事件模块调用的序列化模块不支持传递枚举等各种POD(Plain Old Data)类型. 为了优化开发者的使用体验以及优化事件模块的其他特性, 笔者花了一天的工时对事件模块与序列化模块进行了重构以适应新的需求. 这里讲的内容是对序列化模块的优化思路.

编译期对范型容器类型的识别

上一期的序列化逻辑用了几十行代码实现了不同范型容器的识别, 例如serial::is_tuple, serial::is_pair等, 并且这些范型工具结构基本一致, 重复度很高. 在新版本中, 笔者新增了一种范型工具

1
2
3
4
5
6
7
8
9
template<template <typename ... arg_t> typename container_t, typename first_t, typename ... rest_t>
struct is_container {
static const bool value = false;
};

template<template <typename ... arg_t> typename container_t, typename first_t, typename ... rest_t>
struct is_container<container_t, container_t<first_t, rest_t ...>> {
static const bool value = true;
};

它的作用是传入一个数据类型, 通过特化来判别其是否是容器. 例如

1
2
3
4
5
6
template<typename first_t, typename ... rest_t>
using is_vector = is_container<std::vector, first_t, rest_t ...>;
/* assert failed */
static_assert(is_vector<int>::value);
/* assert success */
static_assert(is_vector<std::vector<int>>::value);

它还可以判别tuple

1
2
3
4
5
6
template<typename first_t, typename ... rest_t>
using is_tuple = is_container<std::tuple, first_t, rest_t ...>;
/* assert success */
static_assert(serial::is_tuple<>::value);
/* assert success */
static_assert(serial::is_tuple<int>::value);

移除类型的常引用修饰符

有时候参数传递不只是某种类型arg_t, 还可能是 const arg_t, const arg_t &, const arg_t &&等. 旧的序列化模块对此处理得不是很好, 重构后新增了一个工具范型

1
2
3
4
template<typename arg_t>
struct remove_operator {
using type = typename std::remove_const<typename std::remove_reference<arg_t>::type>::type;
};

搭配is_container可以实现下面的效果

1
2
3
4
5
6
7
8
9
10
template<typename first_t, typename ... rest_t>
using is_vector = serial::is_container<std::vector, typename serial::remove_operator<first_t>::type, rest_t ...>;
/* assert success */
static_assert(is_vector<std::vector<int>>::value);
/* assert success */
static_assert(is_vector<const std::vector<int>>::value);
/* assert success */
static_assert(is_vector<const std::vector<int> &>::value);
/* assert success */
static_assert(is_vector<std::vector<int> &&>::value);

这样就大大减少了序列化模块的代码量.

POD类型检查

标准库有个接口std::is_pod<>可以作POD类型检查的工作, 在它的加持下serial就支持各种POD类型的参数传递了.

另外…

现在可以通过unpack反序列化字串, 作为std::string_view类型传递

1
2
3
4
5
6
7
8
9
10
11
12
/* ok */
void func(serial::ibuf data) {
auto str = serial::unpack<std::string_view>(std::move(data));
}
/* ok */
void func(serial::ibuf data) {
auto str = serial::unpack<const std::string_view>(std::move(data));
}
/* assert failed */
void func(serial::ibuf data) {
auto str = serial::unpack<const std::string_view &>(std::move(data));
}

注意解包时不可将非 POD 类型的数据按常引用解包, 这是因为常引用的非 POD 对象创建在局部作用域中, unpack返回时无法控制局部非 POD 对象的生命周期. 但所有pod类型的变量都可以解包成常引用, 因为引用的是unpack入参的地址, 入参是serial模块外部的对象, 生命周期可控. 于是对于较大的 POD 数据, 例如std::array<char, 20>, 可以按如下方式解包

1
2
3
void func(serial::ibuf data) {
auto a = serial::unpack<const std::array<char, 20> &>(std::move(data));
}

这样的好处是可以减少拷贝传参的性能损耗.