游戏之作: QVariant的解析和更新

纯属好玩的实现.

我有一个很大的数据结构, 有很多的成员变量, 几十个. 中间涉及到更新, 更新的数据源是外部传过来的一个json对象, 被解析为一个QVariantMapQVariantHash. 对于每个属性, 我们需要判断传进来的Map中有没有包含, 如果包含了, 还要判断是否有意义(例如, 对QString, 是否为空). 只有都满足了, 才会使用它来更新结构中的属性. 属性类型包括整数, 浮点数, 日期类型, 字符串和QByteArray码流. 这里一个很麻烦的问题是, 比如, QString, QDate, 判断值有效性是不同的函数. QStringQByteArrayisEmpty(), 而QDateQDateTime则是isValid().

如果每个都使用if...else判断, 实在太麻烦, 还要记住每个成员的类型. 这个问题只有C++能够给出令人满意的解决方案.

实现0:

可以用宏实现. 这里主要的问题是需要自己处理类型. 仍然需要记住每个变量的转换函数. 在C++11之前, 大概只能这么实现.

实现1:

利用C++17的静态if语句功能, 直接定义一个模板函数:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#define SET_VALID_VALUE(a, b, c) do{ if(c){a=b; }}while(0);

template<typename T, typename M>
void assignFromMap(T& a, const M& b, const QString& c)
{
if constexpr (std::_Is_any_of_v<M, QVariantMap, QVariantHash>)
{
if(!b.contains(c))
{
return;
}
if constexpr (std::is_same_v<T, bool>)
{
a = b.value(c).toBool();
}
else if constexpr (std::is_integral_v<T>)
{
a = b.value(c).toInt();
}
else if constexpr (std::is_floating_point_v<T>)
{
a = b.value(c).toDouble();
}
else if constexpr (std::is_same_v<T, QString>)
{
SET_VALID_VALUE(a, b.value(c).toString(), !b.value(c).toString().isEmpty());
}
else if constexpr (std::is_same_v<T, QByteArray>)
{
SET_VALID_VALUE(a, b.value(c).toByteArray(), !b.value(c).toByteArray().isEmpty());
}
else if constexpr (std::is_same_v<T, QDateTime>)
{
SET_VALID_VALUE(a, b.value(c).toDateTime(), !b.value(c).toDateTime().isValie());
}
else if constexpr (std::is_same_v<T, QDate>)
{
SET_VALID_VALUE(a, b.value(c).toDate(), !b.value(c).toDate().isValid());
}
else{
undeclared(b,c);
}
}
else
{
undeclared(b,c);
}
}

上面的代码中的undeclared()函数是故意留下的一个未实现的函数, 当assignFromMap()函数遇到参数是未定义的类型时, 就会在这里编译出错, 很容易找到问题所在.
如果是C++20, 我们可以利用concept来提高编译器错误信息的可读性, 但是对于这种情况, 远不如这种方式来的更直观. 并且, 我们的产品也只是使用C++17标准, 未迁移到C++20.

就可以这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
auto iter = std::find_if(_impl->_cases.begin(), _impl->_cases.end(), [case_id](const auto& a){ return a._basic._id==case_id; });
RETURN_LOG_IF(iter==_impl->_cases.end(), QString("错误: 找不到case_id=%1的记录!").arg(case_id));
assignFromMap(iter->_basic._case_no, info, "caseNo");
assignFromMap(iter->_basic._summary, info, "diagnosis");
assignFromMap(iter->_basic._age, info, "age");
assignFromMap(iter->_basic._age_unit, info, "ageUnit");
assignFromMap(iter->_basic._identity, info, "identity");
assignFromMap(iter->_basic._sex, info, "gender");
assignFromMap(iter->_basic._sufferer, info, "name");
assignFromMap(iter->_basic._admission_no,info, "admission_no");
assignFromMap(iter->_basic._bed_no, info, "BedNo");
assignFromMap(iter->_basic._department, info, "department");
...

代码逻辑要清晰明了多了.

实现2:

按照C++规范, 静态if的处理仍然是从上到下进行的, 并不是遵循最优匹配原则. 而参考C++17的visitor的实现的overload则是完全按照模板匹配实现的. 下面的实现做了简化, 没有检查参数有效性. 但是基本思想是一样的:

1
2
3
4
5
6
7
template <typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};
template<typename ... Ts>
overload(Ts ...) -> overload<Ts ...>;

定义这个复制函数对象:

1
2
3
4
5
6
7
auto assign = overload{
[](bool& a, const QVariantMap& b, const QString& c){ a=b.value(c).toBool(); },
[](int& a, const QVariantMap& b, const QString& c){ a= b.value(c).toInt();},
[](double& a, const QVariantMap& b, const QString& c) { a=b.value(c).toDouble(); },
[](QString& a, const QVariantMap& b, const QString& c){a=b.value(c).toString(); },
[](QDate& a, const QVariantMap& b, const QString& c){a=b.value(c).toDate(); },
};

QVariant和std::variant

两者有着很大的差异, 虽然都叫variant. 从某种意义上, 或许std::any有点像QVariant.