RTTI 小片段
这里的小例子根据实际业务问题抽象而来,因此可能对抽象后的例子而言基于 RTTI 的实现并非最优。
为了使业务代码更简洁,没有采用可能更好的基于 SFINAE 分发等的实现方法,尽管模板可以很好地处理这里遇到的接口问题。
需求
有多个不同的 Protocol Buffers 结构,都包含同一个字段。针对这个字段,有几种检查条件,每个结构对应的检查条件都不完全一致。
例如,定义 p.proto 如下。
syntax = "proto3";
package p;
message A {
uint64 id = 1;
}
message B {
uint64 id = 1;
}
message C {
uint64 id = 1;
uint32 num = 2;
}
现有两个条件
- 满足
id > 0 - 满足
id为偶数
对 p::A 需满足条件 1,p::B 需满足条件 2,p::C 需同时满足条件 1 和 2。现在需要设计一个函数检查传入的结构是否满足条件。
dynamic_cast 实现
由于三个结构均为 google::protobuf::Message 的派生类,因此可以检查函数传入 google::protobuf::Message 即可。三个结构均有 id() 接口,基类没有,不使用模板的情况下只能用 dynamic_cast。(大概是所谓的 Inheritance Is Not Subtyping 吧……)
bool check(const google::protobuf::Message& msg)
{
if (const auto* ptr = dynamic_cast<const p::A*>(&msg))
return ptr->id() != 0;
if (const auto* ptr = dynamic_cast<const p::B*>(&msg))
return ptr->id() % 2 == 0;
if (const auto* ptr = dynamic_cast<const p::C*>(&msg))
return ptr->id() != 0 && ptr->id() % 2 == 0;
return false;
}
比较简单。但,如果需要检查的结构有数十条,检查条件有十几种的情况下,这种写法肯定就会变成 (极具工程性和扩展性) 的 Copy and Paste 了。
改用表驱动法效果会好一些。首先提取检查函数,每个类型的条件均为一个及以上检查的组合,这样针对类型查表即可。
不用模板的话离不开 RTTI,就需要实现类型到检查函数的映射了。
表驱动法实现
C++ 里类型自然是没办法简单作为 key 的,需要使用 std::type_index。它包装了 typeid 运算符返回的 std::type_info,可用作类型的 key。
表构建好了,仍面临子类型的问题,即,这几个结构在不 dynamic_cast 的情况下不能通过基类调用 id(),也不太可能直接修改 google::protobuf::Message 给它加一个 virtual uint64_t id() = 0; 再给几个派生类的 id() 加 virtual……
还好,Protocol Buffers 提供了简单的反射功能,能根据字段名称取值。这就没问题了,实现如下。
bool check(const google::protobuf::Message& msg)
{
static constexpr auto check_valid = [](uint64_t num) {
return num != 0;
};
static constexpr auto check_even = [](uint64_t num) {
return num % 2 == 0;
};
using check_fn = std::function<bool(uint64_t num)>;
static const std::map<std::type_index, std::vector<check_fn>> table{
{std::type_index(typeid(p::A)), {check_valid}},
{std::type_index(typeid(p::B)), {check_even}},
{std::type_index(typeid(p::C)), {check_valid, check_even}},
};
auto it = table.find(std::type_index(typeid(msg)));
if (it == table.end())
return false;
auto field = msg.GetDescriptor()->FindFieldByName("id");
if (!field)
return false;
return std::all_of(
it->second.begin(), it->second.end(), [&](const auto& fn) -> bool {
return fn(msg.GetReflection()->GetUInt64(msg, field));
});
}
测试
象征性地写一下测试,这里使用 Catch2。
TEST_CASE("Apply different checks for different types", "[check]")
{
SECTION("Test p::A")
{
p::A a;
a.set_id(0);
REQUIRE(check(a) == false);
a.set_id(3);
REQUIRE(check(a) == true);
a.set_id(102);
REQUIRE(check(a) == true);
}
SECTION("Test p::B")
{
p::B b;
b.set_id(0);
REQUIRE(check(b) == true);
b.set_id(3);
REQUIRE(check(b) == false);
b.set_id(102);
REQUIRE(check(b) == true);
}
SECTION("Test p::C")
{
p::C c;
c.set_id(0);
REQUIRE(check(c) == false);
c.set_id(3);
REQUIRE(check(c) == false);
c.set_id(102);
REQUIRE(check(c) == true);
}
}
===============================================================================
All tests passed (9 assertions in 1 test case)
大功告成。