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

本篇源于群友的某个提问,需求略显特别,却是一个非常不错的产生式元编程例子,遂记录一下。 原始问题 优化后的原始问题,代码如下: struct base { virtual void foo() = 0; }; template<int I, int J, int K, int L> struct derived : public base { void foo() override final { std::cout << "derived<" << I << ',' << J << ',' << K << ',' << L << ">::foo()\n";… Continue Reading Clean Conversion from Run-time Values to Compile-time Constants in C++

Introduction 模板是第一阶段元编程最核心的工具,中篇以两章五星难度的内容开头,深入纵览其核心技术与诸般妙诀。本章要讨论的元编程工具——Fold Expressions,依旧处于第一阶段,是 C++17 引入的一个跨越性产生式特性。 该特性从更高层面抽象了参数包拆解方式,消除了传统递归所带来的复杂性,是非常有用的编译期遍历方式。因此,这个特性是产生式元编程中的关键部分,这也是单独分配一章深度讨论的原因。 这个特性也并不完善,C++26/29 依旧会增加与其相关的特性,增加哪些?为什么加?痛点在哪儿?这些问题也将在本章给出答案。 Fold Fold 这个概念来自函数式编程,本身指的是一类高阶函数,这些函数使用给定的组合操作来分析递归数据结构,并通过递归处理其组成部分的结果来重新组合,最终构建出一个返回值。简而言之,Fold 能够递归地遍历数据结构中的每个元素,并通过一个组合函数将这些元素的值合并为一个结果。这个组合函数通常定义了如何将两个值合并在一起,以及如何处理基础情况(如空数据结构)。 这种从递归数据结构中提取信息的数学概念称为 Catamorphism(源自古希腊语:κατά "向下" 和 μορφή "形式,形状"),表示从一个初始代数到其他代数的唯一同态映射。编程中,目标代数通常就是一个单一的值或结果。若从词的本意来理解,其实指的就是将一复杂数据结构向下拆解成简单数据结构的过程,这个过程递归地利用一个组合函数来完成。Catamorphism 通过抽象化递归数据结构的处理方式,提供了一种通用的模式来遍历和处理递归数据结构,使得代码更具有可读性和可维护性,适用于各种场景,如求和、乘积、查找、过滤等。 Fold 只是 Catamorphism 的一个具体实现,主要是指对列表和序列的处理。例如,对列表 [1, 2, 3, 4] 求和,可以这样表示: foldl (+) 0 [1, 2, 3, 4] = (((0 + 1) + 2) + 3) + 4 = 10 foldr (+) 0… Continue Reading 《产生式元编程》第七章 巧活用折叠表达式

T240725 说到,Unnamed namespaces 有一个唯一的名称会通过 using-directives 自动导入,倘若可以手动指定这个名称,就是 inline namespaces 了。此时,自动导入的名称就是 inline namespace 的名称,比如 std::literals 和 std::liternals::chrono_literals 的实现: #if __cplusplus >= 202002L inline namespace literals { inline namespace chrono_literals { /// @addtogroup chrono /// @{ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wliteral-suffix" /// Literal suffix for creating chrono::day objects. ///… Continue Reading T240730 inline namespace

Unnamed namespaces,也叫 Anonymous namespaces,是命名空间的一种特殊形式。这种形式可以省略命名空间的名称,如: namespace { /* .. . */ } 在语义上与等价于: namespace unique_name { /* … */ } using namespace unique_name; 编译器会自动生成一个唯一的名称,并使用 using-directives 自动导入名称。 与其他形式的命名空间不同,Unnamed namespaces 的链接方式是 Internal Linkage,标准描述为: An unnamed namespace or a namespace declared directly or indirectly within an unnamed namespace has internal linkage. All other namespaces… Continue Reading T240725 Unnamed namespaces

前言 模板编程,技巧如云,妙艺似雨。第五章滔滔滚滚地讲述了模板的核心概念和常用技术,篇幅稍长,诸般妙诀,未遑悉录,放于本章。 闲话不题,本章尽是一些巧妙的实践,于极尽模板产生式编程大有用处,难度不低,重要重要! 本章代码非是示例,而是 GMP 库的真实代码。 Type List Type List 是个只装类型的容器,不含任何数据、函数或类型成员,定义相当简单。代码如下: template<typename…> struct type_list {}; 可变模板参数是实现的核心,表示 Type List 中存入的类型。类型并无一致性要求,是以 Type List 本身还是个异构容器。为何需要这种容器?只因编译期可能需要对很多类型操作,借助这种容器,能够方便地保存各种各样的类型。 可是就一行代码,也没见能进行什么操作呀?其实,Type List 只是个类型容器,需要配套算法才能发挥作用,这些算法才是关键。算法可以分成如下几类: 容积(Capacity) 大小(type_list_size_v) 判空(type_list_empty) 元素访问(Element access) 索引访问(type_list_element_t) 前向类型(type_list_front) 后向类型(type_list_back) 尾部类型(type_list_tail) 内含检测(type_list_contains_v) 元素操作(Element manipulation) 合并(type_list_concat_t) 移除(type_list_remove_t) 前向移除(type_list_pop_front) 后向移除(type_list_pop_back) 插入(type_list_insert_t) 前向插入(type_list_push_front) 后向插入(type_list_push_back) 反转(type_list_reverse_t) 去重(type_list_unique_t) 过滤(type_list_filter_t) 搭配以上这些基础算法,Type List 方得显现妙用。以下各个小节,便来展示各个算法的实现,这些实现并不一定是最优的,但却能展示模板技巧。GMP… Continue Reading 《产生式元编程》第六章 感今朝妙艺几人知

前言 本系列分为上中下三个篇章,前四章作为上篇,详细介绍了宏在产生式编程中的原理和应用,本章开始步入中篇,正式进入 C++ 的产生式元编程技术。 宏是 C 时期的产物,功能颇为简陋,亦非图灵完备,即或诸般奇技妙诀加身,限制仍多。且调试不易,有所束缚,以是仅作辅助,用来实现简单的代码生成功能。至 C++ 时期,产生式元编程工具陡增,其中以模板为一切的根基,发枝散叶,尔今正向静态反射前进。 本章涉及不计其数的模板技术,主要集中于模板的核心理论和用法,概念错综复杂,技术层出不穷。难度等级绝对不低,是本系列中非常重要的一章。 纵然某些概念和技术你已知悉,也莫要跳过某个小节,因为本系列的重心不在模板编程,而在产生式编程,论述角度将有所差异。 泛型……元编程 传统过程式编程的思路是将逻辑作为函数,数据作为函数的输入和输出,通过无数个函数来组织完整的逻辑需求,好似搭积木,构建出各种物体。 面向对象编程更进一步,思路是将逻辑和数据合并起来,构成一个个类,类中包含各种数据和函数,外部只能借助公开函数操纵这些数据。这种以对象表示现实事物的方式与人的思维更加接近,简化了抽象化的难度。 泛型编程则欲将逻辑和数据分离,并提供一种抽象化数据的方式,使得不同的数据能够使用同一种逻辑,极大降低了代码的重复性。于是,同一个函数能够传递不同的参数类型,这叫函数模板;同一个类能够传递不同的数据,这叫类模板。 C++ 是一种多范式语言,以模板支持泛型编程,允许编写泛型的函数和数据,功能不依赖于某种具体的数据类型,使用 SFINAE 和 Concepts 约束抽象化的类型。这种能力使其支持 Parametric Polymorphism,在多态的选择上不必局限于传统的 Subtype Polymorphism,可以使用编译期多态替代运行期多态。 C++ 中,元编程指的是发生于编译期的编程,模板为 C++ 带来泛型编程的同时,也带来了元编程能力。由模板实现的元编程,称为模板元编程。而产生式元编程,指的是发生于编译期的对于编程的编程,模板本就支持在编译期生成代码,因此模板也能够实现产生式元编程。 抽象化、具体化……模板 在模板世界中,函数不再是具体的函数,数据类型也不再是具体的数据类型。模板宛如一个模具,借其能够产生各式各样的物体,这些物体的颜色、材料可能不尽相同,但是形状、大小是一致的。也就是说,一类物体,必须存在共同的部分,才能够抽象出一个模具,模具的作用就是重复利用这些共同之处,减少重复性。 编程是工具,本质是解决问题,解决问题考验的就是抽象化和具体化的能力。抽象化应对的是现实世界中的不变,而具体化应对的是现实世界中的变化,能够清楚地认识到变与不变是什么,问题也就迎刃而解了。那先来分清抽象化和具体化的概念。 抽象的意思是,在许多事物中,去除非本质的属性,抽出本质属性。抽象化就是呈现出具体事物共同本质的过程。将复杂的现实,简化成单纯的模型,这种抽象化方式称为模型化,所谓问题建模,就是对问题进行模型化,也即抽象的过程。具体的概念相对简单,就是指看得见摸得着的事物,每个事物都是独一无二的,是以变化尤盛。具体化就是将抽象事物加载清晰的过程,是一种有助于理解陌生事物的方式。数据类型是具体事物,能够清晰简明地呈现出不同数据类型的共同逻辑,就是用模板类型代替具体类型的精妙之处。 模板本身是一种抽象化的工具,以其编写的是抽象逻辑,表示某类类型的共有本质,是不变的部分。其本身是没有用的,实际需要的还是具体逻辑,因此需要有具体化的过程,将抽象逻辑转变为具体逻辑。在 C++ 模板中,这种将抽象逻辑转变成具体逻辑的过程,称为实例化。实例化将在编译期将抽象类型替换成具体类型,根据模板生成一个个具体逻辑。这种实例化机制,便是我们所需要的代码生成能力。 与宏不同,模板具体化后生成的代码,并无法直接在生成的源码中看到,但是可以通过 CppInsight 这类工具察看。 比如: template <typename T> inline constexpr T Integer = 42; int… Continue Reading 《产生式元编程》第五章 忆昔年模板三两事

临时更一篇关于 format 的内容,经验之谈,置为三星。 进入 C++ 标准的库,实践时日往往很久,像 fmtlib、range-v3 这些经典库都已存在十年以上。不受标准牢笼,一个库的发展会快速许多,是以其本身的功能要比加入标准的完善很多。例如 fmtlib,它比 std::format 使用起来更加方便,能直接支持 Formatting Ranges、Formatted Output、Terminal Color 等诸多功能,而这些功能要完全加入标准,可能得等到 C++29 了。 我在某个 C++20 库的开发中就需要使用 format,当时想着 fmtlib 功能更加完善,便没有直接使用 std::format。但后来就遇到了问题,首先是 fmtlib 有诸多版本,版本之间可能存在差异,用户可能并不像我们这般熟悉 C++,编译之时,问题千奇百怪;其次是在用户机器上可能会出现一些莫名其妙的问题,而这些问题放到自己的机器上却不会出现,分析起来将花费大量时间;最后是编译错误信息,fmtlib 开发之时,Concepts 还未进标准,而其采用的定制方式是模板特化,稍微出现一点问题便会弹出满屏的模板错误,而这些错误信息和真实错误毫不相关,加大了定位问题的难度。 总而言之,fmtlib 隐藏的坑不小,换成 std::format 能够避免很多细微的隐患,减少普通用户的抱怨声。 然而,std::format 缺少很多 fmtlib 直接具备的功能,替换也并非那么简单,本篇讲解的就是这些替换的细节。 下面分成三部分关键点进行讨论。 第一,运行期定制。 std::formatter 默认只支持编译期定制,在 fmtlib 中存在 fmt::runtime 可以编写运行期的定制。比如,下面是一个使用 fmtlib 定制 response 的例子:… Continue Reading C++20 std::format 替换 fmtlib 的注意点

宏部分完结 本系列断更良久,去年已更新前三章: 《产生式元编程》 第一章 宏编程计数引原理 《产生式元编程》 第二章 自复用代码生成技 《产生式元编程》 第三章 替换蓝染概念纤悉 宏部分的核心理论和技术,于此三章,悉已更讫。利用这些原理和技巧,可以实现一些简单的代码生成工具,得到初步的产生式元编程能力。 本系列原定十章,宏只是其中的第一个模块,属于上篇。上篇完结后,将产生一个产生式元编程库,再步入中篇。由是,本系列由虚向实,自理论技术诞生项目,复从项目推进续篇,循此迭代。 章章衔进,写来岂是容易?再有诸事相羁,停更半载有余,亦是无奈。近来乘暇推进,初版已卒,遂以续更。 本章乃承上启下之篇,为上篇画句号,为中篇起草稿。 GMP 产生式元编程库 本系列将同步产生一个产生式元编程库,名为 GMP,可在 GitHub 找到 https://github.com/lkimuk/gmp 。该库的侧重点是提供产生式编程工具,可在编译期实现代码生成。 基于前三章的内容,已添加宏模块的基本功能,跨平台,可实现简单的代码生成。其余模块将随着后续章节持续添加。 下面是一些使用例子。 获取可变宏参数数量: #include <gmp/gmp.hpp> int main() { // Output: 0 1 2 3 4 5 6 7 printf("%d %d %d %d %d %d %d %d\n", GMP_SIZE_OF_VAARGS(),… Continue Reading 《产生式元编程》第四章 封装合并框架顿立

本篇介绍几个细琐的小特性,可以使代码更加安全可靠。 最常见的情况是采取 for loop 遍历某个容器,比如: std::vector<int> v(10); std::ranges::iota(v, 0); for (int i = v.size() – 1; i >= 0; –i) { std::cout << v[i] << ' '; } 乍看之下,似乎并无问题,但实际上却存在安全隐患,若是 v.size() 的结果大于 std::numeric_limits<int>::max(),将产生 UB。 倘若你使用了类型推导,问题会更加明显。 for (auto i = v.size() – 1; i >= 0; –i) { std::cout << v[i] <<… Continue Reading 使用 C++20 安全地比较不同类型的整型值

What are the ranking rules for reference bindings? Let’s consider the following example: void f(int);         // #1 void f(int&);        // #2 void f(int const&);  // #3 void f(int&&);       // #4 void f(int const&&); // #5 Here are some key points: #1 is a function that accepts borrowed int arguments. #2 is a function that takes an lvalue reference. #3 is a function that takes… Continue Reading Normal OR Rules for Reference Bindings