临时更一篇关于 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 的注意点

本篇介绍几个细琐的小特性,可以使代码更加安全可靠。 最常见的情况是采取 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 安全地比较不同类型的整型值

霜风凄紧,神寒骨冷,忽忽冬月将尽。忙处更新渐少,今来补上。 本篇讲解如何利用 ChatGPT 快速实现一个控制台进度条小工具,相比单纯介绍某些特性,此种方式涉及知识的综合运用,也顺便谈谈如何结合 AI 进行编程。 问题描述 控制台程序执行一些耗时任务时,需要向用户显示当前任务执行的进度,以提供清晰的感知。比如一个下载程序,通过进度条便能告知用户当前的下载进度。 进度条可以单独显示,也可以在程序输出的最下方显示,下图是一个示例。 这是一种单控制条需求,执行任务,显示进度,输出流依旧是从上至下依序进行,适合单线程的场景。 多控制条显示的效果如下图,实现要更加复杂一些,本文暂时不会涉及该部分。 初步分析 控制台上显示的这种符号,称为 ASCII Art,就是以字符构建的某种图案,不借助图片,也能够有一个生动的展示效果,比如下图这种。 因此控制台进度条也称为 ASCII Progress Bar,通过字符图案来模拟进度条的显示,通常分为已完成部分及未完成部分,使用两个字符,动态改变字符数量,便能够模拟出一个进度条。 模拟方式既定,下一问题在于进度条刷新。如果每更新一次进度,便输出一个字符图案,那么屏幕上将满是进度条,需要针对一条进度条,不断刷新其数据,而非每次都输出一条新的。具体实现时,便需要寻找定位进度条的方法,每次清除当前数据,重新打印新的数据,视觉上显示的是连续动画。 刷新思路亦成,接着的问题在于如何在进度条之上插入其他输出。进度条始终显示在用户输出下方,因此每次用户输出时,可以立即定位到进度条,定位之后清除当前进度条,输出用户内容,再重新打印进度条,便能够达到这一效果。 细枝末节,便需依赖具体的实现手法。 借助 ChatGPT 快速构建基本代码 需求明确,思路既定,接着便要着手设计库的结构和细节,实现细节这部分代码无需从零编写,可以借助 AI 快速生成。 我们所需做的,就是详细描述需求,以及预想的思路,让 ChatGPT 生成代码,验证是否符合需求,若不符合,纠正错误,让它再次生成,不断重复这个过程,直到基本满足期待的效果。如果一开始的效果就完全牛头不对马嘴,那也可以让它基于 Python 生成,等到效果尚可,再让它把代码转换成 C++ 代码。 经过多次调教,最终生成的代码如下: #include <iostream> #include <thread> #include <chrono> void print_progress_bar(int iteration, int total, int bar_length =… Continue Reading 借助 ChatGPT 快速实现一个轻量级的控制台进度条库

设计程序,经常需要分离不变的和变化的逻辑。将不变的逻辑放到一块,再以某种形式为变化的部分提供「定制点」,从而使程序具有更好的可扩展性,同时增加相似逻辑的可复用性。 因此,本质上来说,设计是为了应对变化。通过抽离系统的变化点,再以合适的结构来表示变化,从而控制变化的影响范围。 C++ 提供许多技术来表示定制点,大家最熟悉的就是 OOP 的继承和多态,而 Concepts 也是其中之一。那么 Concepts 为何可以表示定制点?它又产生了怎样的结果?这种方式有哪些好处?又有哪些缺点?与继承的方式有哪些区别?流程有何变化?实际例子有哪些? 这些问题,就是本篇将要讨论的内容。 首先,让我们来探讨一个问题:变化的最小单元是什么? 一个程序是一个系统,一个系统必须具备三个最基本的元素:输入、处理和输出。这三个元素都存在变化,不同的输入对应不同的处理,导出不同的结果。在编程语言中,输入和输出对应「数据」,处理对应「方法」。不同类型的数据需要不同的方法来进行处理,无数个数据和方法构成了整个程序。因此,变化就存在于数据与方法当中。 OOP 通过组块,将数据与方法组合起来,称为一个类,一个类就是一些具有联系的数据与方法的集合。但是,只有一个类无法应对变化,任何变化都会引起对该类的修改,或是重新编写相同的方法。因此,对需要共用某些相同数据或方法的一些类,进行向上一层的抽象,把这些相似的数据或方法放到更高层级,将其他变化的逻辑放到更低层级,使得低层级类可以使用高层级类的共有逻辑,从而提供扩展和复用的能力。通过这种结构化方式,将具备相似关系的类置于一个「层级体系」,高层级的类具有更高的抽象,低层级的类则更加具体。于是,高层级的类可以用来表示接口,低层级的类可以用来定制具体实现,以此来表示变化。「继承」便是用来表示层级关系,而「多态」则是用来重新定义方法。 Concepts是命名的约束,约束其实是一种条件关系,只有满足某些条件才能触发接下来的操作。OOP中,处于同一层级体系的类本身就是一种约束,对于其他类,由于不在该层级当中,因此不符合约束,也就无法使用那些共有的操作了。若要使用这些共有方法,则需要通过继承添加定制点。既然Concepts表示约束,那么就也可以作为一种表示定制点的方式。 那么具体来看,继承和多态是如何表示定制点的呢? 以一个简单的例子来说明: // Dynamic Polymorphism struct Graph { virtual ~Graph() = default; virtual void draw() const = 0; }; struct Circle : Graph { float radius; void draw() const override { std::cout << "draw… Continue Reading 使用Concepts表示变化「定制点」

介绍 今天这篇文章,我想跟大家探索下 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

新年第一篇,好久没写Modern C++主题了,这次来说说C++20的格式化库。 该标准库来自开源库fmtlib,作者为Victor Zverovich,提案为P0645R10。 目前为止,仍旧只有MSVC16.10+对该库支持稍微完整,因此可以先使用 fmtlib。 格式化函数 C++20提供了三个格式化函数,std::format(),std::format_to() 和 std::format_to_n()。 通过一个简单的例子来了解其用法: // format std::cout << std::format("HAPPY NYE {} EVERYONE!", 2022) << '\n'; // format_to std::string buffer; std::format_to( std::back_inserter(buffer), "HAPPY NYE {} EVERYONE!", 2022 ); std::cout << buffer << '\n'; // format_to_n buffer.clear(); std::format_to_n( std::back_inserter(buffer), 6, "HAPPY NYE {} EVERYONE!", 2022… Continue Reading Using C++20 Formatting Library