C++ Extension Functions By Mixin
如果你写过 JavaScript 或者 Kotlin,那么你应该很熟悉可迭代对象的 forEach,filter,map 等方法。这些东西通过链式的调用可以很简洁地表达复杂的逻辑。那么,C++ 怎么做到这一点呢?
本文的代码代码已开源。下文中有些代码单行比较长,单击代码块顶部可以放大代码块
动机
熟悉 C++ 的你,看了我上面的一段话可能第一反应就是:不,你错了,C++ 有这个:
std::vector<int> v;
// 数据初始化
std::for_each(v.begin(), v.end(), [](auto &n){
// 开整!
})
对,没错,for_each 这种基本的操作在 C++ 不存在才是比较匪夷所思的。然而,包括 std::for_each,std::sort 在内的诸多 STL 算法都需要 begin,end 参数。这是为了兼容纯 C 风格的数组,这种数组没有迭代器,也不知道数组大小,除了能通过元素类型推断每个元素占用的内存大小,得不到额外的有效的信息。因此只能限制算法的输入同时包含 begin 和 end,来让算法作用于整个数组。但是,这带来了的后果却是:没有链式调用了。设想一下在 C++ 里面写同样功能的代码:
const tmp = [1,2,3,4,5]
tmp.filter((v) => v % 2 == 0)
.map((v) => v*10)
.forEach((v) => console.log(v))
就算某个平行世界的 C++ lambda 函数的语法简洁得像 JS 一样,连续嵌套多层的调用也不如上面这种链式的调用简洁!
使用方法
以下介绍的两种方法,在包含头文件后,都可以在 IDE 中有完整的类型提示。本方法添加的 mixin,都是静态类型的,所有黑魔法都发生在编译期。
向一个对象添加扩展
using Mixin::mix;
using IterablePatch::ForEachIndexed, IterablePatch::Map, IterablePatch::Filter;
using std::cout, std::endl;
struct ObjStyleTest {
static void run() {
cout << "Object style syntax: mix things into an object." << endl;
std::vector<int> a{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto b = mix<ForEachIndexed, Map, Filter>::in(a);
auto c = b.filter([](auto &value) {
return value % 2 == 0;
}).map([](auto &value) {
return value * 10;
});
c.for_each_indexed([&c](auto idx, auto &n) {
cout << n << (idx < c.size() - 1 ? ", " : "");
});
cout << endl;
// 是的,这玩意可以再赋值给一个普通的 vector
std::vector<int> d = c;
cout << "Testing implicit cast" << endl;
bool flag = true;
c.for_each_indexed([&flag, &d](auto idx, auto &v) {
flag &= v == d[idx];
});
cout << (flag ? "YES!" : "NO!") << endl << endl;
}
};
创建一个扩展后的类型
struct ClassStyleTest {
template<typename T>
using vector = typename mix<ForEachIndexed, Map, Filter>::in_class<std::vector<T>>;
static void run() {
cout << "Class style syntax: mix things into a class" << endl;
// 支持普通的构造函数和/或初始化列表
// vector<int> a(3, 10);
// vector<int> a(2);
vector<int> a{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// ...
}
};
如何自己构造新的扩展
注:可以参考源码中已经写过的 4 个扩展
构造一个模板类,这个模板类的模板有两个参数,分别是基类型和父类型,基类型是被 mixin 的那个类型,父类型是所有 mixin 完成后的那个类型,这两个类型在使用时会被自动推断,因此写的时候不需要关心它们究竟是什么类型(实际上也没办法关心,毕竟,天知道你要把这个类 mixin 到哪里去)。在这个模板类的内部,写一个方法,这个方法就是你希望某个类具有的方法,它将被 mixin 到我们的目标类中。示例代码如下:
template<typename Base, typename Super>
struct Filter : Base {
MIXIN_DEFAULT(Filter, Base)
using value_type = typename Base::value_type;
Super filter(std::function<bool(value_type &)> f) {
Super res;
for (auto &v: *this) {
if (f(v)) {
res.emplace_back(v);
}
}
return res;
}
};
我们希望这个 filter 方法返回 filter 后的数组,那么 filter 函数的返回类型就需要是父类型。此外,正如代码中显示的那样,你可以访问 this 指针。这个类必须实现恰当的构造函数和类型转换,才能保证和原始的 STL 容器以及后续 mixin 了更多的扩展容器不冲突,不过好在所有的类要写的东西都是一样的,你可以直接用 MIXIN_DEFAULT 这个宏一把梭。
原理
其实原理很简单:继承,链式的继承。在链式继承结束后,把继承后的类型作为模板参数写入继承链中的模板类里。
首先,我们构造一些模板类,来帮助我们识别类型,这也是 C++ 常用的技术,在 STL 里满天飞
template<
typename Base,
typename Super,
template<typename, typename> class MixinHead,
template<typename, typename> class ...MixinTail
>
struct ChainedSpec {
using inner_type = typename ChainedSpec<Base, Super, MixinTail...>::type;
using type = MixinHead<inner_type, Super>;
};
template<
typename Base,
typename Super,
template<typename, typename> class MixinHead
>
struct ChainedSpec<Base, Super, MixinHead> {
using type = MixinHead<Base, Super>;
};
利用可变模板参数的特性,我们支持了任意多数目的模板参数。如果 Y 继承了 X,Z 又继承了 Y,那么 ChainedSpec<X, Super, Y, Z>::type 就可以告诉我们 Z 的类型。哦?那 Super 是什么?我们恰恰就会在 Super 这里放上 Z 的类型本身(递归真有趣,不是吗?):
template<typename Base, typename Super, template<typename, typename> class ...Mixin_>
using MixinImpl = typename ChainedSpec<Base, Super, Mixin_...>::type;
template<typename Base, template<typename, typename> class ...Mixin_>
struct mixin : MixinImpl<Base, mixin<Base, Mixin_...>, Mixin_...> {
template<typename T>
mixin(std::initializer_list<T> list)
:MixinImpl<Base, mixin<Base, Mixin_...>, Mixin_...>
(std::forward<std::initializer_list<T>>(list)) {}
template<typename ...Arg>
mixin(Arg &&... arg)
:MixinImpl<Base, mixin<Base, Mixin_...>, Mixin_...>
(std::forward<Arg>(arg)...) {}
};
把所有的类型说明清楚,同时再利用 C++ 的完美转发,把构造函数委托给父类,构造函数会逐级委托,直到基类。到了这里,事情就算完成了。因为我们只是要添加新的成员函数而不是成员变量,而且这个成员函数并不是虚函数(虚函数涉及到虚函数表,虚函数表的取值是取决于对象的),所以只要构造出一个我们需要的类型就可以了。实际的代码中,只要把当前的类型,强制类型转换为这个 mixin 后的类型即可。
那么,最后一步就是写一点辅助函数,帮助我们完成这个类型转换的过程。这个辅助函数有两个版本,一个是把一个对象执行类型转换,另一个版本是输出 mixin 后的类型用于我们创建对象。
template<template<typename, typename> class ...Mixin_>
struct mix {
template<typename Base>
inline static constexpr auto in(Base base) {
return static_cast<detail::mixin<Base, Mixin_...>>(base);
}
template<typename Base>
using in_class = typename detail::mixin<Base, Mixin_...>;
};