C++98元编程技术解析

人们往往会将一个大问题拆解成许多小问题,通过解决一个个小问题,最终就能解决整个大问题。
若是拆解过后,这些小问题的处理逻辑不变,变化的只是输入状态,那么此时就是一种代码复用。对应编程世界,一种是自上而下的拆解组合方式,称为递归;一种是自下而上的拆解组合方式,称为迭代。
拆解后还能组合,拆解才有意义,递归和迭代本身就带有一种约束,必须具备起始状态和终止状态。若是没有起始状态,递归就没有起点,循环就没有开始;若是没有终止状态,递归就没有终点,循环就没有结束,
本文所说的元编程拆解技术,就是编译期的问题拆解与组合技术,编译期不像运行期那样能够动态地改变输入与输出状态,c++ 诞生了许多技术来解决这个问题,这便是后文要介绍的。
本文以一个需求为例,来讲解这些技术:
要求编写一个 unroll(callback) 函数,该函数将调用 n 次传入的 callback 函数。
起始状态就是 n,终止状态就是 n 为零,拆解后处理逻辑不变的小问题就是 callback。
先不看文,你首先想到的是什么解法?文读毕,对比一下文中的各种拆解技术思路,获益更多。
原始递归法
模板元编程最开始就只支持递归这一种拆解方式,每次输入一个状态,依次产生下一个状态,若状态与递归终止状态相同,则结束。
采用这种方式,实现需求如下:
1namespace cpp98 { 2 3    // declaration 4    template  void unroll(f); 5 6    template  7    struct unroll_helper { 8        void operator()(f f) { 9            f();10            unroll(f);11        }12    };1314    // terminated state15    template 16    struct unroll_helper {17        void operator()(f) {}18    };1920    // definition21    template 22    void unroll(f f) {23        unroll_helper()(f);24    }2526    void print_cpp98() {27        std::puts(hello cpp98);28    }29}3031int main() {32    cpp98::unroll(cpp98::print_cpp98);33    // output:34    // hello cpp9835    // hello cpp9836}  
由于函数模板不支持偏特化,于是需要借助一个 unroll_helper 类模板,来实现递归终止条件,再在 unroll 函数中调用该帮助类,不断拆解问题。
递归输入条件 n 为起始状态,每次拆解执行完成之后,通过 n - 1 得到下一个状态,从而不断解决问题。
这个时期,c++ 的元编程拆解技术还很弱,一个简单的需求,实现起来也颇为繁琐。
可变参数模板
时间来到 c++11,模板元编程迎来强大扩展,非常有用的一个新特性就是可变参数模板,它能够让我们摆脱递归这种原始拆解方式。
但是 c++11 还只是开始,基础组件不完善,所以并不能非常有效地实现目标。
什么意思呢?看如下这个不太完善的实现:
1namespace cpp11 { 2 3    template  4    class index_sequence {}; 5 6    template  7    void unroll(f f, index_sequence) { 8        using expand = std::size_t[]; 9        expand{ (f(), is)... };10    }1112}131415int main() {16    cpp11::unroll([] { std::puts(hello cpp11); },17        cpp11::index_sequence());18}  
原始递归法是采用不断递归来动态地产生状态,而有了可变参数模板,状态可以直接在编译期初期产生,从而直接拿来用就可以。
这里定义了一个 index_sequence 用来接收所有状态,然后借助一些逗号表达式技巧展开参数包,在参数包展开的过程当中,执行处理逻辑。
c++11 起也支持 lambda,因此也不用再提供一个额外的调用函数。
这个实现的唯一缺点就是由于缺乏相应的组件,需要手动产生状态,导致使用起来较为麻烦。
完善版可变参数模板
c++14 增加了 std::index_sequence 和 std::make_index_sequence,于是就能将手动产生状态变成动态产生,完善实现。
代码如下:
1namespace cpp14 { 2 3    template  4    void helper(f f, std::index_sequence) { 5        using expand = std::size_t[]; 6        expand{ (f(), is)... }; 7    } 8 9    // variable template10    template 11    auto unroll = [](auto f) { // generic lambda12        helper(f, std::make_index_sequence{});13    };14}1516int main() {17    cpp14::unroll([] { std::puts(hello cpp14); });18}  
同时,c++14 还支持 variable template 和 generic lambda,这进一步简化了实现。
fold expression
前面的方式是采用逗号表达式技巧来展开参数包,c++17 支持 fold expression,可以直接展开,因此代码得到进一步简化。
变成:
1namespace cpp17 { 2 3    template  4    void helper(f f, std::index_sequence) { 5        ((f(), is), ...); // fold expression 6    } 7 8    template  9    auto unroll = [](auto f) { // generic lambda10        helper(f, std::make_index_sequence{});11    };12}  
constexpr if
c++17 的另一种拆解技术是借助 constexpr if,它的好处在于能够直接在本函数内判断终止状态,这样就不再需要去定义一个递归终止函数。
1namespace cpp17 { 2    // variable template + constexpr if 3    template  4    auto unroll = [](auto expr) { 5        if constexpr (n) { 6            expr(); 7            unroll(expr); 8        } 9    };10}1112int main() {13    cpp17::unroll([] { std::puts(hello cpp17); });14}  
与原始递归法相比,这种方式除了消除递归终止函数,还免于编写一个额外的 helper 类,generic lambda 更是减少了模板参数。
这是目前为止,最简洁的实现。
c++20 双层 lambda 法
有没有非递归的简洁拆解方式呢?当然也有。
看如下实现:
1namespace cpp20 { 2 3    template  constexpr auto unroll = [](auto f) { 4        [f](std::index_sequence) { 5            ((f(), void(is)), ...); 6        }(std::make_index_sequence()); 7    }; 8} 910int main() {11    cpp20::unroll([] { std::puts(hello cpp20); });12}  
这里的关键是 c++20 的 template lambda,它支持为 lambda 编写模板参数,基于此才能够编写索引的模板参数。
lambda 函数里面再套一个 lambda 函数,外层用于提供调用接口,内层用于管理状态和处理调用。如果没有 template lambda,内层 lambda 的 std::index_sequence 参数就无法写,也就接收不了状态。、
structured binding packs
原本有些新特性是应该在 c++23 就进入标准的,但由于种种原因,我们只有期望 c++26 能用上了。structured binding packs 就是这么一个特性。
前面除了递归以外的所有拆解方法,都得借助 std::index_sequence,这就是代码依旧复杂的原因所在。
有没有一种方式可以直接让我们访问参数包,而不必再定义一个参数为 std::index_sequence 的函数才能拿到那些参数包?structured binding packs 就提供了这一能力。
这是 p1061 所提出的一种方式,让我们能够通过 structured bindings 直接得到参数包。
于是实现变为:
1namespace p1061 { 2 3    template  constexpr auto unroll = [](auto f) { 4        auto [... is] = std::make_index_sequence(); 5        ((f(), void(is)), ...); 6    }; 7 8} 910int main() {11    p1061::unroll([] { std::puts(hello p1061); });12}  
这种拆解技术才是最直观的方式,两行代码解决一切。
expansion statements
另外一种方式就是我们在反射中经常使用到的一个特性:template for。
这种方式比 structured binding packs 更强大,是静态反射里面的一个扩展特性,能够支持编译期迭代。
对于本次需求的实现为:
1namespace p1306 { 2    template  constexpr auto unroll = [](auto f) { 3        constexpr std::array dummy{}; 4        template for (auto& e : dummy) 5            f(); 6    }; 7} 8 9int main() {10    p1306::unroll([] { std::puts(hello p1306); });11}  
这里借助了 std::array,构建了一个并不会实际使用的变量,目的是为了当作遍历次数。
总结
本文从 c++98 开始介绍了许多拆解技术,在不断的优化过程中,也能够看到 c++ 的发展历程。
由最原始的复杂、难用,到最后的两行代码搞定,也能够看到 c++ 元编程的发展。
利用好这些技术,对大家的元编程能力会有显著提高。


VR电力事故模拟体验系统,通过沉浸式体验感受安全生产的重要性
聊聊三极管CE组态的低频等效电路、低频截频及对应的伯特图
中国人工智能产业分析报告以及发展趋势
!@@!HP8648A,HP8648B超低价!!现货欢迎来电
智能门锁已成为了目前值得信赖的安全管家
C++98元编程技术解析
微软发布Surface Duo双屏设备,预装Android系统
利尔达物联网入选2023年浙江省高新技术企业研发中心名单
华为数字能源发布新一代高效智能充电40kW直流充电模块
3D打印技术助力开启民营航空
如何进行监控系统组网
浙江移动推出基于5G+MEC“连接+算力+能力”工业增强一体化方案
Python数据挖掘:WordCloud词云配置过程及词频分析
全球TOP15半导体厂商今年第一季度营收出炉
伺服驱动器输出电压的测量方法及注意事项
宽带功率放大器的结构原理和如何实现应用设计
阿特斯N型大面积多晶太阳电池采用P5高效电池技术,转换效率达23.81%
op07可以单电源工作吗?
2023年无人机行业发展的十大趋势
雷达模组在智能门锁中的雷达应用