CppMore
Dive deep into the C++ core, and discover more!
CppMore

这篇写个平时易被忽略的小知识点,一元 + 操作符的使用技巧。 一般二元 + 操作符用得较多,只有一个操作数时,没人会多此一举地把 1 写成 +1。 不过若是操作数为整数或无作用域枚举类型,一元 + 操作符会执行 Integral promotion,此时会发生隐式转换。例如: // unscoped enumeration enum Enum : unsigned int { enum_val_a, enum_val_b, enum_val_c }; int main() { bool b = true; +b; // int +enum_val_b; // unsigned int char c = 'c'; +c; // int unsigned short… Continue Reading “+” 的几个使用小技巧,你了解吗

衔引 原理毕,复用续。历观编程概念,避及重复,提其不变,于迭代与递归为甚。产生式元编程,乃欲自动产生百千代码,迭代递归,自是重要组件,不可不备。 本章以 FOR_EACH 为例,渐次派生问题,引出技术要点,实现强大组件。 迭代 FOR_EACH 用来迭代数据集合,依次取出数据,后调用函数处理。参数易定,一个回调函数加上可变参数。代码表示: #define FOR_EACH(call, …) // Invoke FOR_EACH(Foo, 1, 2, 3) 实现需逐个取出可变宏参数,调用 Foo() 处理。上章已讲解重载之实现法,此乃条件逻辑在宏中的表示法。无循环时,递归思想便是拆解参数包的唯一方式,将重载实现法与递归思想结合起来,即可达逐个取参数之效。 遂可如此表示: #define _FOR_EACH_1(call, x) call(x) #define _FOR_EACH_2(call, x, …) call(x) _FOR_EACH_1(call, __VA_ARGS__) #define _FOR_EACH_3(call, x, …) call(x) _FOR_EACH_2(call, __VA_ARGS__) #define _FOR_EACH_4(call, x, …) call(x) _FOR_EACH_3(call, __VA_ARGS__) #define _FOR_EACH_5(call, x, …)… Continue Reading 《产生式元编程》第二章 自复用代码生成技

引言 自 C 以来,宏为代码生成工具。至 C++98 则有模板,泛型编程日益盛行。迄于 C++20,引编译期表达式,添 Concepts,元编程基础支持始渐完善。由此元编程之技稍简。而静态反射乃元编程系统之核心,却迟久未至,产生式元编程遂仍繁复。 所述元编程之书文,指不胜屈,其间也以编译期计算为主,奇技淫巧,小大靡遗。而于产生式元编程,言者寥寥,常见于库中直用。于是有此系列,略述浅见,供同道者读阅之。 产生式元编程,即为编译期代码生成的技术,各类系统,特性不侔,用法与能力亦有所殊。是以本系列跨度稍大,然只涉标准,不论自成之法,预计十章左右,从旧有到未来之特性,俱可囊括。 问题 代码生成以宏为先,虽是旧工具,然方今模板元编程的能力尚有不足,在产生式元编程的核心特性「源码注入」进入标准之前,它仍是一种常用的代码生成工具。 宏编程得从可变宏参数开始谈起,可变模板参数的个数可以通过 sizeof…(args) 获取,宏里面如何操作呢?便是本章的问题。 接着便逐步分析问题,实现需求,其间穿插宏编程之原理。 分析与实现 宏只有替换这一个功能,所谓的复杂代码生成功能,都是基于这一核心理念演绎出来的。故小步慢走,循序渐进,便可降低理解难度。 问题是获取宏参数包的个数,第一步应该规范过程。 过程的输入是一系列对象,对象类型和个数是变化的;过程的输出是一个值,值应该等于输入对象的个数。如果将输入替换为任何类型的其他对象,只要个数没变,结果也应保持不变。 于是通过宏函数表示出过程原型: #define COUNT_VARARGS(…) N 根据宏的唯一功能可知,输出只能是一个值。依此便可否定遍历迭代之类的常规方式,可以考虑多加一层宏,通过由特殊到普遍的思想来不断分析,推出最终结果。 #define GET_VARARGS(a) 1 #define COUNT_VARARGS(…) GET_VARARGS(__VA_ARGS__) 目前这种实现只能支持一个参数的个数识别,可通过假设,在特殊的基础上逐次增加参数。于是得到: #define GET_VARARGS(a, b) 2 #define GET_VARARGS(a) 1 #define COUNT_VARARGS(…) GET_VARARGS(__VA_ARGS__) 若假设成立,通过暴力法已能够将特殊推到普遍,问题遂即解决。但是宏并不支持重载,有没有可能实现呢?通过再封装一层,消除名称重复。得到: #define GET_VARARGS_2(a, b) 2 #define GET_VARARGS_1(a) 1… Continue Reading 《产生式元编程》第一章 宏编程计数引原理

宏编程的基本原理都已暗含于解决前两节问题的过程当中,本节开始,依此继续展开。 经由前面的努力,我们已掌握条件逻辑在宏编程的表示,代码生成往往涉及循环,所以今天来讲如何实现一个 FOR_EACH。 FOR_EACH 用来迭代数据集合,依次取出数据,然后调用函数处理。因此,参数很容易确定,一个回调函数加上可变参数。代码表示: #define FOR_EACH(call, …) 至于展开参数,可以利用重载技术加上递归思想,因为重载技术在前文已经有相关组件,所以实现起来非常简单。 #define _FOR_EACH_1(call, x) call(x) #define _FOR_EACH_2(call, x, …) call(x) _FOR_EACH_1(call, __VA_ARGS__) #define _FOR_EACH_3(call, x, …) call(x) _FOR_EACH_2(call, __VA_ARGS__) #define _FOR_EACH_4(call, x, …) call(x) _FOR_EACH_3(call, __VA_ARGS__) #define _FOR_EACH_5(call, x, …) call(x) _FOR_EACH_4(call, __VA_ARGS__) #define FOR_EACH(call, …) \ OVERLOAD_HELPER(_FOR_EACH_, COUNT_VARARGS(__VA_ARGS__))(call, __VA_ARGS__) 通过层层递归,便可以把大问题拆分成一个个小问题。如果数据集合大小为 5,那么就从… Continue Reading T230925 Generative Metaprogramming with Macro Preprocessor (Part 3)

经过上篇分析实现,第一个需求「计算可变宏参数个数」已由 COUNT_VARARGS 基本实现。 让我们先总结一下用到的思想和发现的技术,再进入下一步。 2.1 通过增加一个间接层,能够解决无法直接解决的问题。 2.2 小步快走,由特殊逐渐扩展到普遍,能够降低问题的解决难度。 2.3 规范过程,确认变与不变,逐步控制变量,能够全面分析问题。 2.4 尝试改变输入的顺序、大小、个数…… 也许能有新发现。 2.5 初步发现规律时,扩大样本验证,能够将特殊推到普遍。 2.6 可变宏参数作为输入和欲输出结果组合起来,其间规律可以表达条件逻辑。 2.7 扩展编译器及语言版本,能够更全面地测试解决方案的普遍性。 那么本篇就来进一步完善解决方案,使它支持 C++20 以下版本。 问题的关键在于 ##__VA_ARGS__ 不具备通用性,此时一种精妙的技术是分情况讨论,即零参和多参分别处理。考虑上节分析过程中的一条失败道路: #define GET_VARARGS_2(a, b) 2 #define GET_VARARGS_1(a) 1 #define GET_VARARGS(…) GET_VARARGS_X(__VA_ARGS__) #define COUNT_VARARGS(…) GET_VARARGS(__VA_ARGS__) 这是函数重载的思路,由于必须确定 GET_VARARGS_X 中的 X,而 X 又和可变宏参数相关,可变宏参数又属于无限集,因而无法确定这个 X,导致此路不通。但是,如果改变前提条件,使 X 的值属于有限集,这条路便可走通,这里便能够利用起来。 只考虑零参和多参的情况,X 的取值范围就可以定在 0… Continue Reading T230920 Generative Metaprogramming with Macro Preprocessor (Part 2)

这期开始将写一些关于宏编程的内容,讲解宏作为代码生成工具在产生式元编程中的运用。 有人可能会有疑问,已有模板作为元编程工具,为何还需要使用宏这种古老的代码生成工具?自然是因为如今模板元编程的代码生成能力仍有不足,在第三阶段 C++ 元编程的核心工具「源码生成」进入标准之前,宏依旧是一种常用的代码生成工具。 文章定位是 TGS,所以每 Part 只会讲一个单独的技术点,利于循序渐进地吸收。 宏编程得从 Variadic Macros 开始谈起,可变模板参数的个数可以通过 sizeof…(args) 获取,宏里面如何操作呢?便是本篇的主题。 首先需要明确,宏只有替换这一个功能,所谓的复杂代码生成功能,都是基于这一核心理念演绎出来的。因此,只要小步慢走,层层递进,复杂能力其实也并不复杂。 我们的需求是获取宏参数包的个数,第一步应该规范过程。 过程的输入是一系列对象,对象类型和个数是变化的;过程的输出是一个值,值应该等于输入对象的个数。如果将输入替换为任何类型的其他对象,只要个数没变,它的结果应该保持不变。 于是通过宏函数表示出过程原型: #define COUNT_VARARGS(…) N 根据宏的唯一功能可知,输出只能是一个值。依此便可否定遍历迭代之类的常规方式,可以考虑多加一层宏,通过由特殊到普遍的思想来不断分析,推出最终结果。 #define GET_VARARGS(a) 1 #define COUNT_VARARGS(…) GET_VARARGS(__VA_ARGS__) 目前这种实现只能支持一个参数的个数识别,我们通过假设,在特殊的基础上逐渐增加更多参数。于是得到: #define GET_VARARGS(a, b) 2 #define GET_VARARGS(a) 1 #define COUNT_VARARGS(…) GET_VARARGS(__VA_ARGS__) 如果该假设成立,通过暴力法已能够将特殊推到普遍,问题也就解决了。但是,宏并不支持重载。有没有可能实现呢?通过再封装一层,消除名称重复。 #define GET_VARARGS_2(a, b) 2 #define GET_VARARGS_1(a) 1 #define GET_VARARGS(…) GET_VARARGS_X(__VA_ARGS__)… Continue Reading T230917 Generative Metaprogramming with Macro Preprocessor (Part 1)

今天讲消息分发的一种编译期实现法。 编程是一门非常依赖逻辑的学科,逻辑分为形式逻辑和非形式逻辑,编程就属于形式逻辑。形式逻辑指的是用数学的方式去抽象地分析命题,它有一套严谨的标准和公理系统,对错分明;而日常生活中使用的是非形式逻辑,它不存在标准和公理,也没有绝对的对与错。 根据哲学家大卫·休谟在《人性论》中对于观念之间连接的分类,我们能够把逻辑关系分成三大类:相似关系、因果关系、承接关系。相似关系表示两个组件结构相同,去掉其中一个组件,也只会使功能不够全面,并不会影响程序;因果关系表示两个组件之间依赖性极强,没有第一个组件,就没有第二个组件,第二个组件依赖于第一个组件;承接关系表示两个组件都是局部,只有组合起来,才能构成一个整体。 消息分发就属于因果关系,我们需要依赖 A,去执行 B,没有 A 就没有 B。同样属于因果关系的术语还有逻辑分派、模式匹配、定制点的表示方式等等,它们本质都是在描述一类东西,只是有时候侧重点不同。 条件关系也属于因果关系的范畴,是编程中逻辑最重的关系。试想没有 if else,你还能写出多少程序?世界是复杂的,问题也是复杂的,因果关系必不可少。 消息分发,或称为逻辑分派,就是一种简化条件关系表达方式的技术。它适用于存在大量因果的情境,此时若是使用原始的 if else,则无法适应动态发展的世界。 C++ 中,最典型、也非常有用的一种方式就是采用 map,因作为 key,果作为 value,因是标识符,果是回调函数。由于这种方式发生于运行期,所以也称为动态消息分发。 本文要讲的,是 C++20 才得以实现的另外一种方式,发生于编译期的静态消息分发技术。 称为消息分发,一般是在网络通信的情境下。正常情境下,程序是顺序执行的,所以完全可以使用 if else 来实现因果逻辑,因为组件与组件之间距离较近,属于同一模块;而网络情境下,一个组件可以瞬间跳跃到距离非常远的另一个组件,这两个组件甚至不在同一台设备上,一台设备可能在上海,另一台在北京,此时如何让这两个组件进行沟通?也就是说,A 组件里面的某个函数执行条件不满足,如何简单地跳到 B、C、D、E…… 这些组件的某个函数中去处理?这种远距离的程序因果逻辑,通过消息分发组件能够非常丝滑地表示。 消息分发的标识符一般采用字符串表示,到了 C++20 支持 string literal NTTP 才得以在编译期实现一套可用的相关组件。 因此首先,我们得实现一个 string literal 以在编译期使用。 template <std::size_t N> struct string_literal { // str is… Continue Reading Compile time dispatching in C++20

今天这篇讲 Monads,对 C++ devs 来说是一个比较新的概念,也是一个比较难理解的概念。本文将概念和实践串起来讲,先讲抽象理论,后面讲具体实现和使用,以全面理解其定位。 Language 编程语言用于操作机器以辅助人类解决问题,主体是人和机器,目的是解决问题。人通过机器解决问题,编程语言是人和机器之间沟通的桥梁。 问题解决最基本的流程为定义问题、分析问题、寻找策略及选择策略。简单来说,你有一些东西,想通过一些行为,再得到一些东西。问题就是理想和现实之间的差距。 这其中最基本的构成要素就是东西和行为,也就是编程语言当中的数据和函数。一个大问题不可能通过一个行为就能解决,往往需要拆分为一个个小问题,再逐个击破,故根据问题复杂程度,数据和函数的数量也会随之变化。 主体虽分为人和机器,但却是由机器来完成具体工作。最初的语言,靠机器更近,尽管效率高,人和机器沟通起来却是多有不便;随后的语言,便靠人更近,使人和机器的沟通方式,更加接近人与人之间的沟通,这才简化了编程难度。 常言道,月满则亏,水满则溢,凡事过犹不及。太靠近机器的语言,尽管效率高,但却难以理解;太靠近人的语言,虽说易理解,效率却颇低。因此,只有在明确仅注重效率或简易的情况下,才会使用这类语言,许多语言其实都处于中间,保持效率的同时也避免太过复杂。 Imperative versus Declarative 由机器到人,编程语言是把机器所能识别的具体指令,抽象为人所能理解的词句。 声明式语言相比命令式语言,离人更近,命令式更关注具体,声明式更关注抽象。抽象是把不变的都封装起来,从而避免去写一个个重复的指令,比如原本想要修改 std::vector 的元素,你需要使用 if、for 等等指令,这些再往上抽象一层就是过滤和变换两个步骤,直接使用 v | filter | transform 则更加言简意赅。因此,声明式用起来要更加简洁,使人更加专注于问题解决步骤。 具体中包含了变与不变,抽象中提取了不变的部分。具体就像是白话文,抽象就像是文言文,前者通俗易懂,后者简洁精炼。命令式让人更加专注于行为的细枝末节,事无具细,一切尽在掌握之中;声明式让人更加关注于行为步骤,抽离细节,一切环环相扣。 也可以说命令式注重问题解决过程,而声明式注重问题解决结果。不关心过程,只想要结果,展现在语言上就是只调用接口,不关心实现。因此,从视角上来看,命令式更偏向于微观,声明式更偏向于宏观。宏观适合思考问题,微观适合解决问题,前者像是将军,只谋战略,后者则像是小兵,具体实施。 将无兵而不行,兵无将而不动。抽象与具体当中,虽然真正起作用的是具体的指令,但使用的却是抽象的接口。接口使来结构清晰,能更加专注于问题逻辑,跳出问题细节。抽象与具体紧密连接,只要抽象,抽象从何来?只要具体,具体何以用? 是以范式不同,本质是解决问题思维的不同,是侧重点的不同。 What is Monads C++ 有许多范式,面向过程、面向对象、泛型编程、函数式编程…… 许多人发现 Modern C++ 写来越发简单,这种简单写法就谓之 Modern,但这种简洁从何而来?本质就是范式的转变。 C++11 至今,C++ 已逐步从函数式编程中汲取了许多特性,Monads 就是其中非常重要的一个特性。C++23 中也加入不少 Monads,简化了一些原本较为繁琐的操作。 那么,什么是 Monads? 这个概念来自范畴论,称为单子,牵扯概念颇多,且不易理解,因此我们结合 C++… Continue Reading Monads in Modern C++, What, Why, and How

今天再来看看 C++23 Monadic std::optional,在 Overview of C++23 Features 只是简单介绍了用法,这里来说说设计原理。 std::optional 是一个 Monad,这个概念源于 FP (Functional Programming),对于 C++ coder 而言,理解起来并非易事。若是你熟悉 Haskell,这个就相当于里面的 Maybe。Monads 叫作单子,是范畴论里面的概念,若要完全弄懂的话需要扩展许多概念,不符合 TGS 的定位,因此这里只是小做介绍。 简单来说,Monads 是 FP 当中的一个抽象数据类型,抽象的目的在于表示计算过程。通过这种方式,能够减少重复代码,简化组合流程。 用下面的公式来解释一下: m a \rarr (a \to m b) \rarr m b (a \rarr m b) 表示操作,a 是输入类型,b 是输出类型。操作必须满足某种上下文,才能知道怎样处理,m 就指定了这个上下文。如果将其替换为一个针对 std::optional 的函数,会更加容易理解: std::optional<int> f(std::optional<std::string>… Continue Reading T230807 Reviewing C++23 Monadic std::optional

What and Why 这次单独说一下 std::exchange,它是 C++14 <utility> 提供的一个函数模板,实现很简单。 template<class T, class U = T> constexpr // since C++20 T exchange(T& obj, U&& new_value) noexcept( // since C++23 std::is_nothrow_move_constructible<T>::value && std::is_nothrow_assignable<T&, U>::value ) { T old_value = std::move(obj); obj = std::forward<U>(new_value); return old_value; } 看实现可知其逻辑很简单,就是设置新值、返回旧值。但却难以顾名思义,它实际上并不会交换数据,那得这样写: y = std::exchange(x, y); 上篇说过,std::exchange 和… Continue Reading std::exchange use cases