最近发现泛型编程有了另一利器——泛型 Lambda,比想象当中要强大许多,威力不小,和大家分享一下。 本篇内容需要对泛型编程有所了解,若是读过之前更新的相关文章,食用更佳。 泛型编程 开始之前,先来简单回顾一下泛型编程的内容。 泛型编程的目的是将「数据和方法」进行分离,将数据高度抽象,于是可以表示同类问题的「最小通解」。 C++ 通过模板来实现泛型编程,模板又分为变量模板、函数模板和类模板。这些模板始终围绕着「数据和方法」。变量模板属于对数据类型的抽象,函数模板属于对方法的抽象,而类模板,则二者兼有,因为类本身的目的就是将数据和方法进行结合。因此为什么说函数模板处理的是数值,而类模板处理的是类型呢?就是由于函数只具有方法,而在C++中方法是不支持偏特化的,所以它无法处理类型。 至 C++14,Lambda 也迎来了泛化能力,称为 Generic Lambda。不过此时的泛化能力只是由 auto 带来的,威力略弱。随后又经过多年的发展,Lambda 的能力越来越强。C++20 加入了 Template Lambda,这让 Lambda 也可以指定模板参数,使其泛化能力更加完善。 至此,C++ 的泛型编程多了一个新的主角——泛型 Lambda。 泛型 Lambda 为何泛型Lambda值得单独拿出来说呢? 一是因其特殊性,在一些情境使用它来封装变化,会让事情简单许多;二是由其新颖性,它的许多特性和用处尚处探索期,值得讨论。 首先来说其特殊性。Lambda 函数其实就是一个匿名的函数对象,它实际上也是一个“类”。不同的是,它唯一的方法就是 operator(),也就是 Lambda 体,而数据则是 [] 中捕获的参数,这些参数就是“类”中定义的成员变量。因此,Lambda 函数既具有函数的部分特征,又具有类的部分特征。也因如此,事情变得有趣起来。Lambda 具有的函数部分特征,让它具备了函数模板的能力;类部分特征,让它具备了类模板的继承能力。此外,由于 Lambda 的类型是一个 closure type(闭包类型),所以它还可以定义在函数内部,也可以当作回调函数使用。 如此这些,再加上泛型,使得泛型 Lambda 极具威力。 继而来看其新颖性。 当下大多数 C++ 开发者对于 Lambda 的使用,还只是停留在函数部分,相当于只发挥了… Continue Reading 泛型 Lambda,如此强大!

根据提案 P0634-Down with typename,C++20 之后 typename 在有些地方不再必要。 原文主要内容如下: If X::Y — where T is a template parameter — is to denote a type, it must be preceded by the keyword typename; otherwise, it is assumed to denote a name producing an expression. There are currently two notable exceptions to this… Continue Reading 新简化!typename 在 C++20 不再必要

介绍 今天这篇文章,我想跟大家探索下 Attributes 这个概念。 如果你还没有听过这个概念,或是一知半解,没咋用过,那正好表明它处于一个被忽略或是低估的位置。 Meeting C++ 曾经对此做过一份调查,结果如下: 可以看出,大概一千人填写了这份问卷,其中就有半数人表示从未用过 Attributes,在被使用的 Attributes 当中,使用频率也相差较大。 你可能会认为,这些特性大都是针对写库或大型项目的人准备的,它们只是针对一些特定的场景进行优化,普通开发者几乎用不上。 然而,事实真的如此吗? 许多C++编译器不仅仅实现了语言的核心特性,还通过扩展提供了一些额外特性,比如 GNU 提供的__attribute__,MSVC 提供的 __declspec。编译器可以根据这些扩展的特性进行一些优化,但由于这些特性和平台绑定,使用这些特性就会影响代码的可移植性。因此,C++ 标准从 C++11 就开始把一些有用的扩展,慢慢添加到标准中来。 这些添加进来的扩展就叫做 C++ Attributes,标准对语法进行了统一,使用 [[attr]] 或是 [[namespace::attr]] 来指定普通的或是带有命名空间的 Attributes。 那么为什么要采用新语法,而非引入新的关键字呢?一是可以降低 Attributes 加入的障碍,二是可以防止关键字泛滥。 我们的大脑在处理事务时,是需要区分「背景」跟「主体」的,若所有的 Attributes 都被定为关键字,那么势必引发关键字泛滥。当一切都成为了主体,就相当于一切都是背景,突出不了重点。 打个比方: 在一个 RPG 游戏中,包含许多剧情,这些剧情不能整体都非常平淡,也不能整体都是高潮。因为我们对于这个游戏的整体记忆,取决于它剧情高潮和结尾时的体验。剧情越是跌宕起伏、有高有低,越能够给玩家留下深刻记忆,玩家也就越会倾向于评价这个游戏好玩。 游戏里的这些重点剧情就是「主体」,过渡剧情就是「背景」。背景是为主体服务的,去除它并不会影响整个剧情。 同样,是否使用 Attributes 也并不会影响程序的语义,也就是说,即使编译器忽略一个 Attribute 也完全没有坏处。 顺便一提,在早些时候,override 和 virtual 本来是作为… Continue Reading 那些值得使用的 C++ Attributes

C++20 新增了两个 const 相关的关键字,于是当前存在四个相似的关键字:const,constexpr,consteval 和 constinit。 接下来分别来进行讨论。 第一,经过 const 修饰的变量具有只读属性,并且初始化发生于运行期。也就是说,若一个变量定义之后不允许被修改,就应该给它加上 const。若在一个成员函数中不修改任何成员变量,就应该在成员函数后面加上 const。但是,它也可能发生于编译期,例如以 const int 代替宏来定义数组大小。 第二,经过 constexpr 修饰的变量或是函数,既保证只读,又发生于编译期。然而,只有在参数是常量,和显式地以其返回值来初始化一个编译期常量时,它修饰的函数才会一定发生于编译期。如: constexpr int sqr(int n) { return n * n; } int main() { // compile time static_assert(sqr(10) == 100); // compile time int array[sqr(10)]; // compile time constexpr int res = sqr(10);… Continue Reading Differences between keywords constexpr, consteval and constinit

Introduction 程序设计需要不断地做抉择,抉择便需用到逻辑分派。 Modern C++ 中,有多种方式完成这个任务,例如 Run-time if,Tag dispatching,SFINAE,Partial Specialization 等等。这些方式分为运行期分派和编译期分派,分派的条件称为约束。设计是为了厉行约束,理想上,能在编译期强制表现的约束就应提升到编译期完成。 C++17 又增加了一种编译期方式 Compile-time if;C++20 中,又增加了 Concepts。 本篇就来总结一下各种方式的用法异同(Concepts见下篇)。 Run-time if 运行期 if 就是运行时期才会执行的分支语句,例如: template <typename T> void foo(const T& val) { if(std::is_integral<T>::value) { std::cout << "integral\n"; } else { std::cout << "non-integral\n"; } } if-else 和 switch 都可以在运行期进行逻辑分派,大部分时候的执行期成本亦微不足道。但是 if-else 要求每一个分支都得编译成功,即使不会执行到的分支,比如返回两个不同类型值的时候,便会出现编译错误。 下面,来看一个贯穿本文的例子。… Continue Reading Simplify Code with “if constexpr” in C++17

本篇作为 Understanding variadic templates 的进阶内容,同时,Fold Expressions 也是 C++17 最常用的特性之一。 Fold Expressions 的基本概念 C++11中,参数包只能在需要参数列表的上下文展开,比如函数递归。而递归函数需要终止条件,因此往往需要提供一个同名的函数来终止递归。 举个例子: void print() { std::cout << '\n'; } template<typename F, typename… Args> void print(F first, Args… args) { std::cout << first << ' '; print(args…); } 我们无法在函数主体中展开,例如不能这样做: template<typename… Args> void print(Args… args) { std::cout << … <<… Continue Reading C++17: Simplify Code with Fold Expressions

GP and Templates(泛型编程与模板) 现实中许多问题错综而复杂,解决起来极为不易。 人们发现可以将这些问题拆解成更小的问题来进行解决。往往当把问题拆解到最小模块的时候,便能对问题产生新的理解。 能解决问题的根本在于:问题整体可分解为部分,部分可组成为整体,整体等于部分之和。这是还原论的思想,在编程世界叫作分治策略,实现手法分为迭代和递归。 向下拆分,向上总结,就能把一个大问题化为许多小问题,通过解决一个个小问题,最终就能解决整个大问题。 解决之时,需要描述问题的数据和关系。 在C++中,用面向对象来表示事物的整体结构,用数据结构来表示数据之间的关系,用算法来表示具体的逻辑实现。 数据有类型之分,而不同的问题可能本质上实为同类问题,这便导致每次遇到同类问题,还得提供不同类型版本的解法。 因此数据和方法应该进行分离,对数据进行更高层次的抽象。 面向对象编程的目的是将数据和方法关联起来,而为了分离数据和方法之间的依赖,出现了泛型编程。 泛型编程本质上就是对数据类型的高度抽象,使同一个方法能作用到不同的类型,为同类问题提供了一个通解。 C++泛型编程所提供的支持组件便是模板,而Variadic Templates(可变参数模板/可变模板参数),顾名思义,就是支持任意个数、任意类型的模板。 对于一类问题,若发现变化的永远是输入,而解决方法始终不变,便可以使用可变参数模板定义通解。 这个通解,就是问题的「最小模块」,通过迭代或递归,分多次解决分解后的小问题,最终就能解决整个问题。 然而迭代需要持有一个迭代器并改变它,直到某个条件符合。泛型编程发生于编译期,编译期中的整数计算(如enum)或是typedef(using)的类型定义之后就无法进行改变。所以迭代虽然比递归表示起来更加自然,却无法进行实现。 递归符合一切编译期编程的要求,所以可变参数模板由其实现。 由于递归本身的特性,再结合可变参数模板,使得理解起来极为不易。因为你无法看到实际的结果,对于无法看到的东西,理解起来总是比较困难的。 Ellipsis operator and Parameter pack(省略操作符与参数包) 先来看一个简单的可变参数模板示例: template <class… Args> void func(Args… args) { std::cout << sizeof…(args) << std::endl; } … 叫做省略操作符(ellipsis operator),用以表示任意个参数,带省略号的参数称为参数包(Parameter pack)。 当省略操作符出现在参数名(args)左边时,标示着参数是一个参数包;出现在参数名右边时,用于扩展参数包。因为我们无法直接获取参数包 args 中的每个参数,所以只能通过展开的方式来获取,那么如何展开参数包就成了难点所在。 现在 func 函数可以接受任意个参数: func<>… Continue Reading Understanding variadic templates

上篇介绍了 C++20 协程的诸多内容,独余 co_await 未曾涉及,它是协程中非常重要的一个关键字,用以支持挂起(suspend)和恢复(resume)的逻辑。 本篇便专门来对其进行介绍。 Awaitable type and Awaiter type 较于普通函数,协程支持挂起和恢复。那么何时挂起,何时恢复,便是逻辑之所在。 由于许多问题在解决时都无法立刻得到答案,即结果存在延迟性,在程序中就表现为阻塞。阻塞会导致CPU大量空闲,效率大减,于是就要想办法实现非阻塞。 多线程便是解决阻塞的一个方式,遇到阻塞,便由操作系统进行切换调度,以此实现非阻塞。重叠 IO 亦是一个非阻塞方案,遇到阻塞,提供一个回调函数给操作系统,系统在阻塞完成后调用其继续执行。 这些方案,本质上都是在处理如何挂起和恢复的问题。换言之,就是在遇到阻塞时暂停当前工作,先去进行别的工作,等阻塞完成后再回来继续完成当前工作。 既然拥有共同的处理问题的逻辑,那么对其进行抽象,便能得到一个高层级的类型。这个高层级的类型便是 Awaitable type(即上篇的 Awaitable object)。 简单地说,Awaitable type 就是对阻塞问题进行总结、归纳、提炼要点,所得到的模型。 那么有何好处呢? 好处就是,我们只要依照抽象后所得模型中的一些规则,便能定义出所有类似问题的解决逻辑,所有类似问题都能依此模型进行解决。 乍听很复杂,其实并不难,只要依规则行事便可。 那么具体规则又是什么? 其实只是三个接口: await_ready() await_suspend() await_resume() 它们分别代表着:是否阻塞、挂起、恢复。 将程序中阻塞完成的条件,写到 await_ready 函数中,便能依此决策何时挂起,何时恢复。 一个类型若直接实现了这三个接口,那么这个类就被称为 Awaiter。 什么意思呢?若类型 A 本身并未实现这三个接口,而是通过类型 B 实现的,那么类型 A 就称作 Awaitable,类型 B 称作 Awaiter。… Continue Reading C++20 Coroutines: operator co_await