在实现向量执行引擎的过程中,有大量不同类型,语义相同, 代码结构很相似的函数, 比如:
- +, - , *, /, %等算数运算.
- 类型之间相互cast的函数.
- 各种builtin函数.
这种函数,经过不同参数的排列组合下来, 数量非常庞大. 编写这么规模庞大函数,需要投入很大的精力. 同时, 后期维护成本也很大, 比如:
- 增加一个新类型,比如增加Array,Map, Struct等类型。
- 重构某一个类型,比如Decimal128的基础上,支持Decimal64, Decimal32, Decimal256.
- 发现为某一种具体运算的更有的向量实现,需要变更算法. 比如,double转string的函数需要变更,而tinyint,smallint,int等类型的转string函数保持不变.
怎么处理这么一大堆函数,满足下面条件:
- 代码类型安全: 如果类型错误,编译时的静态检查,可以给出报错提示.
- 代码的boilerplate要少: 重复的代码不利于维护,或者维护时,容易漏写.
- 代码的局部可调整性要好:代码足够的灵活,随时可以抽取一个特例,进行改写.
总之, 代码的逻辑上,要能够做到incremental refine.
本文作者起先尝试,使用模板重构DorisDB(incubator-doris的商业化版本), 但操作的过程中发现,直接使用模板,还是无法降低这种冗余. 由此想到了函数式编程语Haskell的模式匹配中有guard的机制,guard可以进一步帮助函数做更加精细的匹配. C++20里面有了Concept能够实现类似的功能, 但是C++20的concept概念,笔者多次尝试看文档,任然感觉concept语法构造不够直观,况且目前还没有广泛采用, 并且我们的c++标准目前还停留在c++17标准. 所以,我们探讨c++17的前提下,怎么实现guard机制.
为了双面guard机制的必要性, 举个例子:
比如,你要实现 float,double,tinyint,smallint,int,bigint, largeint, decimal32,decimal64,decimal128这么多类型的 add,sub,mul,div,mod,bitand,bitor,bitnot,bitxor等等函数
这些函数大同小异, 可以用一个模板的来实现, 可以了一个通用的版本, 直接用+,-, *,/, &, |, ^这些算符 ,因为很多类型已经做了重载.
struct AddOp{};
struct SubOp{};
struct MulOp{};
struct DivOp{};
struct ModOp{};
...
template<typename Op, typename Type>
struct BinaryArithmeticOperator{
Type apply(Type const& lhs, Type const& rhs);
}
但是float和double的取模运算没法直接用%,而是用gcc的builtin函数fmod,你需要添加一个模板的特例:
你怎么添加呢?
template<> BinaryArithmeticOperator<ModOp, float> {...}
template<> BinaryArithmeticOperator<ModOp, double> {...}
这两个模板只是一个参数不一样,其他完全一样, 代码重复冗余;能否合并成一个一个模板特例呢?
template<typename T> BinaryArithmeticOperator<ModOp, T> {...}
显然这样做的话, 其他类型也会匹配到这个模板,这符合预期. 因此需要一个guard机制, guard是一个类型表达式,只能接受float和double;其他类型直接fail掉,根据sfinae原则,fail掉之后,其他类型依然可以尝试匹配其他模板特例.
template<T> BinaryArithmeticOperator<ModOp, T> Guard(T is float or double){...}
具体在实现过过程中, 我们将上面函数写成下面形式:
struct AddOp{};
struct SubOp{};
struct MulOp{};
struct DivOp{};
struct ModOp{};
...
template<typename Op, typename Type, typename=guard::Guard, typename=guard::Guard>
struct BinaryArithmeticOperator{
Type apply(Type const& lhs, Type const& rhs);
}
template <typename T> using ModOpGuard = guard::TypeGuard<T, ModOp>;
template <typename T> using FloatTypeGuard = guard::TypeGuard<T, float, double>;
template<typename Op, typename Type>
struct BinaryArithmeticOperator<Op, Type, ModOpGuard<Op>, FloatTypeGuard<Type>>{
Type apply(Type const& lhs, Type const& rhs);
}
通过上述方法,可以将float和double的mod实现用一个模板特例实现.
假如,后续实现decimal32,decimal64, decimal128的mod运算,我们只要添加:
template <typename T> using DecimalTypeGuard = guard::TypeGuard<T, Decimal32, Decimal64, Decimal128>;
template<typename Op, typename Type>
struct BinaryArithmeticOperator<Op, Type, ModOpGuard<Op>, FloatTypeGuard<Type>>{
Type apply(Type const& lhs, Type const& rhs);
}
假如后续添加decimal256呢? 也不能实现.
在具体实现过程中,还需要为non-type template parameter 提供ValueGuard.
关于TypeGuard和ValueGuard的实现和Demo,请参考:
实现:
https://github.com/satanson/cpp_etudes/blob/master/include/guard.hhgithub.comdemo:
https://github.com/satanson/cpp_etudes/blob/master/unittest/test_guard.ccgithub.com上述代码,在g++ 10.2.0和clang++ 11.0.0上执行OK,行为正确.
额外需要了解我们工作,或者对我们工作感兴趣的,年末打算换工作搞数据库的同学,我们可以私信沟通.