发布时间:2026/8/26 19:10:45
基于 C++11 标准实现 Python 风格的 print 函数,打印任意数量任意类型 一个适配标准库容器及其嵌套支持任意类型、任意数量输入并且极简新增自定义类型适配的 print目录先看效果Print: 起源强迫症要做就做个各项目通用的 print 函数第一步解决任意数量参数——可变参数模板第二步类型分流——对标准库容器及数据结构的处理第三步类型分流——未知的类型怎么办细节控的自我修养那些专门处理的边角第四步性能优化——写都写了不如写快点1. 自定义缓冲流 AutoOStream2. 整数查表绕开 ostream3. 浮点格式化用上了 Dragonbox妥协与遗憾结尾大梦初醒先看效果我想要的 print大概长这样当然实现的效果也是这样std::vectorintnums{1,2,3};intarr[3]{4,5,6};std::vectorstd::mapint,std::stringnested{{{1,a},{2,b}},{{3,c}}};std::tupleint,float,std::stringtp{7,8.12345,Hello};MyStruct s;// 假设有个没适配 operator 的自定义类型glily::io::print(nums:,nums,arr:,arr,nested:,nested,tp:,tp,pi:,3.14,Unknown:,s);输出nums: {1, 2, 3} arr: {4, 5, 6} nested: {{1:a, 2:b}, {3:c}} tp: {7, 8.12345, Hello} pi: 3.14 Unknown: MyStruct:0x7ffe06db1967一次调用基本类型、容器、嵌套容器、连没适配过的自定义类型都能打。这就是我想要的体验。由于实现会考虑所有标准库可遍历容器、C数组、tuple、C字符串、函数指针及其相互嵌套的全覆盖并且针对性优化了性能。这些能力的实现代码量不小因此难以一次性展示源码详见BokuMeidoCpp。Print: 起源我大学期间只用 Python 做深度学习工作后因为实际部署需要才开始学 C。刚上手时最让我难受的不是指针、不是内存管理而是——没有 print。在 Python 里print(anything)就完事了list、dict 嵌套多少层都能打。而到了 C我想看一眼std::vector的内容得写循环for(constautox:vec)std::coutx ;std::coutstd::endl;写一次两次还行可调试时经常遇到类似的情况甚至vector 里套 map、map 里套 vector每次都要写循环、想格式写多了快要张口翻白眼。当时我只有一个念头想把我常用的打印操作整合成一个 print 函数像 Python 那样什么东西都能往里扔——这就是开头先看效果里的样子。强迫症要做就做个各项目通用的 print 函数其实给特定项目写个打印函数很容易但我有个习惯——我只想写好一次然后所有项目都能用并且行为一致。所以给自己定了三个目标——这些目标前后花了两年才在功能层面实现支持任意类型、任意数量的参数像 Python 一样标准库容器直接打印嵌套容器也能打实在打印不了的输出ClassName: Address而不是编译报错第 3 点是很重要的。Python 的 print 就是这样——什么都能打打不了也不崩。我希望 C 也能给我这种感觉调试的时候不用担心有某个类型导致编译错误。第一步解决任意数量参数——可变参数模板C 要怎么实现输入不固定数量的参数这是让 print 的输入方式开始像 Python 的地方也是我接触模板编程的开始。任意数量的参数如何依次处理呢我的方法是用一个初始化列表展开参数包让每个参数进入专门的处理函数osInputtemplateclassT,class...Argsvoidprint(constTarg,constArgs...args){_priv::osInputFloatPrecision()-1;_priv::StringOutBufsbuf_priv::getPrintSbuf();// 线程本地的字符串缓冲sbuf.clear();std::ostreamos_priv::getPrintOStream();os.clear();// 复位状态位避免异常残留的 badbit 导致后续输出静默丢失_priv::AutoOStreamaos(os);_priv::osInput(aos,arg);// 第一个参数单独打印inttmp[]{0,(aos.put( ),_priv::osInput(aos,args),0)...};// 展开剩余参数参数间以空格分隔(void)tmp;aos.put(\n);aos.flush();conststd::stringssbuf.str();std::cout.write(s.data(),static_caststd::streamsize(s.size()));// 整条输出一次性写入 coutstd::cout.flush();}int tmp[] {0, (aos.put( ), _priv::osInput(aos, args), 0)...};这种写法我第一次遇到时觉得很难理解。其实它的作用就是把剩余参数args…展开每个都调用一次osInput并写入分隔空格第一个参数arg在展开之前单独处理。展开全部发生在编译期。AutoOStream是我对ostream的一层封装1.0.1 里它接的是线程本地的 ostream其底层是StringOutBuf字符串缓冲至于为什么要封装涉及更后面的性能优化问题先把它当成cout理解即可。另外注意这个实现没有锁。1.0.0 版本用一把全局 mutex 保护输出1.0.1 改用thread_local缓冲每个线程先把整条输出拼进自己的字符串最后std::cout.write()一次性写入。这样既消除了锁竞争又不会和其他线程的cout输出交错。第二步类型分流——对标准库容器及数据结构的处理参数数量解决了现在的问题是怎么让标准库容器等分别打印最初的想法很直接把标准库里所有容器的osInput重载都写一遍在函数体内挨个打印元素。但这又有个问题嵌套容器怎么办而且你不知道它嵌套了多深。遇到这种情况我自然想到了递归——不直接用cout打印元素而是递归调用osInput递归下去总会到达一个基础类型。以标准 STL 容器的重载为例// 添加对 STL 标准容器的支持// 虽然用了双层模板 CTer但单层也可以达成目的// 这里的 enable_if 是编译期判断类型是否具备标准的 begin()/end()templatetemplateclassU,class...UsclassCTer,classT,class...Ts,typenamestd::enable_if_priv::StdBeginEndCheckerconstCTerT,Ts...::value,int::typeinlinevoidosInput(AutoOStreamaos,constCTerT,Ts...cter){aos.put({);for(autoitcter.begin();it!cter.end();){_priv::osInput(aos,*it);it;if(it!cter.end())aos.write(, ,2);}aos.put(});}StdBeginEndChecker是什么暂时不用细讲只要知道它可以判断一个类型是否具有标准的 begin() 和 end() 方法。现在对于基本类型、标准库容器及其他数据结构对应的打印逻辑如下算术类型→ 走整数和浮点数各自的快速转字符串算法字符类型直接打印指针→ 除了字符串指针外直接输出指针地址0x12345678函数→ 输出函数类型名如void (*)(int)标准库容器等数据结构→ 输出{1, 2, 3}格式递归处理元素数组→ 类似标准库容器递归打印map 系列→ 输出{key:value, key:value}格式键值对分别递归打印第三步类型分流——未知的类型怎么办就算我能重载所有标准库的数据类型也不可能穷举所有第三方类型。难道遇到未知的类型我就报错吗这样的话print 也只是勉强能用功能并不完备。怎么处理未知类型呢仔细想想这些类型无非就两种可以被cout打印的和不能被cout打印的。于是我用一个模板来区分这两种情况// 支持 operator 的类型 → 直接打印// 这一长串 enable_if 条件是在编译期做类型分流细节不用深究templateclassT,typenamestd::enable_iftype::StdCoutEachCheckerconstT::value!_priv::IsFuncOrMemFuncCheckerT::value!std::is_pointerT::value!std::is_arithmeticT::value,int::typeinlinevoidosInput(AutoOStreamaos,constTarg){aosarg;}// 不支持 operator 的类型 → 打印 ClassName: AddresstemplateclassT,typenamestd::enable_if!type::StdCoutEachCheckerconstT::value,int::typeinlinevoidosInput(AutoOStreamaos,constTarg){aos.put();conststd::stringtype_nametype::getTypeNameT();aos.write(type_name.c_str(),type_name.size());aos.put(:);aosstd::showbasestd::hexuintptr_t(arg)std::dec;aos.put();}同样的StdCoutEachChecker及后续复杂的模板判断细节暂时不用深究只要知道这些模板区分了类型T是否支持cout 操作。现在对于其他类型就只有这两种情况了支持operator的类型→ 直接打印都不支持的→ 打印ClassName: Address顺理成章地想要适配其他类型无需看懂osInput的实现无需自行添加模板特化无需学习任何新概念——只需要为你想要打印的类型重载operator即可。这也是我选择cout路径的原因这套设计适配新类型极为方便能降低心智成本。顺带一提这个适配会在我的format和toStr函数中同步生效——它们的底层与print共用osInput。细节控的自我修养那些专门处理的边角写完主体后我花了很多时间处理边角情况。这些细节没啥技术含量但决定了一个工具好不好用char系列char、signed char、unsigned char本质是整数但打印时应该当字符不能打出数字char*字符串指针打印字符串本身而不是地址volatile char*也要逐字符打出来其他指针打印十六进制地址带0x前缀C 数组char数组当字符串打其他数组打{...}tuple需要编译期索引递归取值用std::integral_constantbool在编译期判断递归终止函数指针 / 成员函数没法打印值那就打印函数类型名——总比报错强。第四步性能优化——写都写了不如写快点功能齐全之后强迫症又犯了性能。虽然 print 主要是调试用的但性能能省则省更何况format等函数对性能也有要求。于是做了三件事1. 自定义缓冲流 AutoOStream直接往std::cout打每次都是一次虚函数调用。我包了一层 512 字节的栈缓冲小片段先攒着填满了才一次性写入底层流classAutoOStream{public:voidwrite(constchar*s,size_t len){if(pos_lensizeof(buf_))// 缓冲放不下{flush();if(lensizeof(buf_)/2)// 长字符串直接写避免拷贝{os_-rdbuf()-sputn(s,len);return;}}memcpy(buf_pos_,s,len);pos_len;}private:charbuf_[512];// 栈上缓冲size_t pos_0;std::ostream*os_;};1.0.1 里print给AutoOStream接的是线程本地的ostream底层StringOutBuf拼完一整行后一次性写入cout——顺带解决了多线程输出交错的问题。2. 整数查表绕开 ostream整数不经过ostream自己写转换——一次处理两位数字查00~99的静态表while(uval100){size_t pairstatic_castsize_t(uval%100);uvalstatic_castUT(uval/100);constchar*ddigits2(pair);// 两位数字查表buf[--pos]d[1];buf[--pos]d[0];}3. 浮点格式化用上了 Dragonbox浮点格式化是个深坑。标准库的to_chars是 C17 才有的C11 下只能用snprintf慢。后来我找到了 Dragonbox——目前浮点转字符串最快的算法之一把它嵌了进来。满足 IEEE 754 的浮点走 Dragonbox不满足的比如某些平台上的 long double回退到snprintf输出结果超出缓冲区的再走ostream的operator——总之不遗漏特殊情况。妥协与遗憾有几个地方是我没处理或刻意没处理的写出来给大家避坑宽字符 / 宽字符串直接按整型打印。cout 本身不支持宽字符我也没有打算造一套自己的宽字符体系。print 不可重入自定义类型的operator里不能调用 print 本身会出问题。冗余的 CTer 双层模板早期为了匹配 STL 容器写的template template class U, class... Us class CTer, ...后来发现单层模板也能做到。但因为它是编译期逻辑、没有运行时开销就一直没简化——既然没问题还看着很厉害就不动了。结尾大梦初醒费了千辛万苦随着对 C 的理解越来越深我实现的 print 函数也越来越接近 Python 的体验——那我可一定要狠狠使用它然而等我写完这个 print我发现已经完全习惯了 C反而不太需要 print 函数了。悲但这个结果也不赖至少我技术是学会了实现 print 时积累的osInput类型分流体系后来成了我实现format、toStr的核心。现在我的库里这些都能用autos1glily::str::toStr(std::vectorint{1,-2,3});// {1, -2, 3}autos2glily::str::toStr(std::mapint,std::string{{1,a},{2,b}});// {1:a, 2:b}autos3glily::str::format({} × {} {},6,7,6*7);// 6 × 7 42而且浮点格式化实测与 {fmt} 在同一量级、部分场景还略快一点Dragonbox 的功劳。这段经历让我明白了一件事想偷懒可能是最好的学习动力。如果不是为了不写循环我可能到现在还停留在能实现项目需求就行的阶段不会去碰模板编程也不会理解 SFINAE、类型约束这些东西。如果你也刚学 C或者也想要一个能打任何东西的 print——完整实现就是我开头提到的那个轻量纯头文件库BokuMeidoCpp零依赖#include bokumeido/core.hpp就能用format、log、线程池都在里面。后续文章format/toStr 如何与 print 共用一套类型分流、log 的同步/异步设计取舍、以及如何让模板错误出现在调用处——感兴趣可以关注我我们下篇见。如果这篇文章对你有帮助欢迎点赞、收藏、关注、star你的支持是我继续分享的动力

相关新闻

2026/8/26 19:05:44

机器学习——Day05

一、K-近邻算法1.K-近邻算法简介1.1什么是K-近邻算法根据你的“邻居”来推断出你的类型是一种分类算法1.2K-近邻算法(KNN)概念定义:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一…

2026/8/26 19:05:44

如何选择人生第一套房

1、户型与房屋性质1一梯一户是首选2一梯两户选西户3两梯两户选东户4两梯四户选边户5两梯五户换个盘6三梯六户安置房通过小区名称区分房屋的性质:湾临水房苑安置房园刚需房城大盘房院高端盘府亲奢房2、如何选择房子1)根据自己的经济状况做出一个合理的买房…

2026/8/26 19:05:44

AI如何重构ToB获客逻辑?拓氪科技数据智能获客闭环值得选吗?

AI赋能获客的核心变革价值,在于彻底革新传统ToB行业“人工拓客、被动等待、低效获客”的传统模式,依托数据找人、算法筛客、智能转客的数字化全新逻辑,实现企业获客效率的跨越式、指数级提升。优质的AI获客理念,离不开成熟、落地、…

2026/8/26 19:55:55

深入理解C++ 智能指针std::unique_ptr<二>自定义删除器

自定义删除器 (Custom Deleter)&#xff1a;unique_ptr 允许通过模板参数指定删除器。由于其类型包含删除器&#xff0c;因此可以在编译期绑定&#xff0c;无额外运行时开销。#include <iostream>#include <memory> // unique_ptr// 一个简单的测试类class Test {p…

2026/8/26 19:55:55

元初混沌体系 第三卷 卫星互联网全域周天拓扑体系:第二十六篇 同轨内链路持续连通稳态设计

第二十六篇 同轨内链路持续连通稳态设计承启前置 设计立论第二十五篇已完成低轨星座激光/微波双链路全域拓扑架构定型&#xff0c;打破西方单链路体制短板&#xff0c;建立激光主传、微波兜底、场景适配、动静制衡的全域传输体系&#xff0c;实现低轨星座空间拓扑骨架与传输链…

2026/8/26 19:55:54

元初混沌体系 第三卷 卫星互联网全域周天拓扑体系:第二十八篇 低轨星座分域自治周天分区规则

第二十八篇 低轨星座分域自治周天分区规则承启前置 规则立论第二十六、二十七篇已完整落地低轨星座同轨静态稳态、跨轨动态灵变的全域链路体系&#xff0c;彻底解决单轨微观链路零抖动永续连通、多轨宏观链路无感知自适应切换两大核心难题&#xff0c;实现低轨星间链路动静二元…

2026/8/26 9:13:28

[光学原理与应用-521]:对光的错误理解与纠偏

首先光是一种能量的载体和形态&#xff0c;宏观上观察到的光是由无数个微观的光量子组成的&#xff0c;每个光子在产生的瞬间&#xff0c;其在真空的空间中以确定不变的速度沿着一个初始的方向一直向前&#xff0c;在微观层面&#xff0c;每个光量子的运动轨迹是以波函数所展现…

2026/8/25 11:48:27

SIP通话转接原理与REFER方法实战解析

1. 通话转接不是“挂断再拨号”&#xff0c;而是SIP会话的动态重定向你有没有遇到过这样的场景&#xff1a;客服坐席A正在和客户通电话&#xff0c;突然需要把这通对话无缝转给专家坐席B&#xff0c;客户完全感知不到中间的断连——既没听到忙音&#xff0c;也没被要求重新拨号…

2026/8/25 16:56:43

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

1. 为什么选择Kolla-ansible来部署单节点OpenStack&#xff1f;如果你正在寻找一种能把OpenStack从“概念”快速变成“可用的实验环境”的方法&#xff0c;那么Kolla-ansible几乎是当前最主流、最省心的选择。我见过太多人卡在手动编译依赖、配置服务、处理版本冲突的泥潭里&am…

2026/8/26 0:04:32

Python random 模块常用函数详解:从入门到实战

目录 1. 引言2. 准备工作3. 基础随机函数4. 序列相关函数5. 随机种子与复现6. 实战案例7. 注意事项8. 常见问题与排查9. 总结 1. 引言 摘要&#xff1a; 本文系统介绍 Python 标准库 random 模块中最常用的随机数生成函数。内容涵盖基础随机函数&#xff08;random()、unifor…

2026/8/26 1:19:35

JSON总结

JSON概念 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式&#xff0c;主要用于跟服务器进行交换数据。它基于ECMAScript的一个子集。 JSON采用完全独立于语言的文本格式&#xff0c;但是也使用了类似于C语言家族的习惯&#xff08;包括C、C、C#、Java、JavaScr…

2026/8/26 1:19:35

保存连接sse 是什么原理,为什么不会一直请求

“保持连接”用的是 SSE&#xff08;Server-Sent Events&#xff09;&#xff0c;本质是一个没有马上结束的 HTTP 请求。 过程是&#xff1a; 拷贝机发送一次请求&#xff1a; GET /api/code-sync/events服务器返回&#xff1a; Content-Type: text/event-stream但不关闭响应&…

2026/8/26 19:34:06

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站&#xff0c;核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测&#xff0c;千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队&#xff0c;覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/26 19:17:08

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站&#xff0c;核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测&#xff0c;千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队&#xff0c;覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/26 19:34:05

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具&#xff0c;覆盖选题构思、文献整理、内容生成、格式排版等核心场景&#xff0c;真正帮你高效搞定论文难题。 一、全流程王者&#xff1a;一站式搞定论文全链路&#xff08;一天定稿首…