C++ Extension Functions By Mixin

发布于 2022-03-08  98 次阅读


如果你写过 JavaScript 或者 Kotlin,那么你应该很熟悉可迭代对象的 forEach,filter,map 等方法。这些东西通过链式的调用可以很简洁地表达复杂的逻辑。那么,C++ 怎么做到这一点呢?

本文的代码代码已开源。下文中有些代码单行比较长,单击代码块顶部可以放大代码块。

动机

熟悉 C++ 的你,看了我上面的一段话可能第一反应就是:不,你错了,C++ 有这个:

std::vector 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_...>;
};

终有一日, 仰望星空