精华内容
下载资源
问答
  • 2021-05-22 09:23:07

    一.简介

    架构设计中最重要的两个文档的模板和关键说明。这个案例文档仅给出一些关键内容供你参考,部分细节无法全面覆盖或者完全保证正确。(斜体字是示例)

    二.备选方案模板

    2.1 需求介绍

    [需求介绍主要描述需求的背景、目标、范围等]

    随着微博业务的不断发展,业务上拆分的子系统越来越多,目前系统间的调用都是同步调用,由此带来几个明显的系统问题:

    • 性能问题:当用户发布了一条微博后,微博发布子系统需要同步调用“统计子系统”“审核子系统”“奖励子系统”等共 8 个子系统,性能很低。
    • 耦合问题:当新增一个子系统时,例如如果要增加“广告子系统”,那么广告子系统需要开发新的接口给微博发布子系统调用。
    • 效率问题:每个子系统提供的接口参数和实现都有一些细微的差别,导致每次都需要重新设计接口和联调接口,开发团队和测试团队花费了许多重复工作量。基于以上背景,我们需要引入消息队列进行系统解耦,将目前的同步调用改为异步通知。

    2.2 需求分析

    [需求分析主要全方位地描述需求相关的信息]

    2.2.1 5W

    [5W 指 Who、When、What、Why、Where。Who:需求利益干系人,包括开发者、使用者、购买者、决策者等。When:需求使用时间,包括季节、时间、里程碑等。What:需求的产出是什么,包括系统、数据、文件、开发库、平台等。Where:需求的应用场景,包括国家、地点、环境等,例如测试平台只会在测试环境使用。Why:需求需要解决的问题,通常和需求背景相关]

    消息队列的 5W 分析如下:

    Who:消息队列系统主要是业务子系统来使用,子系统发送消息或者接收消息。

    When:当子系统需要发送异步通知的时候,需要使用消息队列系统。

    What:需要开发消息队列系统。

    Where:开发环境、测试环境、生产环境都需要部署。

    Why:消息队列系统将子系统解耦,将同步调用改为异步通知。

    2.2.2 1H

    [这里的 How 不是设计方案也不是架构方案,而是关键业务流程。消息队列系统这部分内容很简单,但有的业务系统 1H 就是具体的用例了,有兴趣的同学可以尝试写写 ATM 机取款的业务流程。如果是复杂的业务系统,这部分也可以独立成“用例文档”]

    消息队列有两大核心功能:

    • 业务子系统发送消息给消息队列。
    • 业务子系统从消息队列获取消息。

    2.2.3 8C

    [8C 指的是 8 个约束和限制,即 Constraints,包括性能 Performance、成本 Cost、时间 Time、可靠性 Reliability、安全性 Security、合规性 Compliance、技术性 Technology、兼容性 Compatibility]

    注:需求中涉及的性能、成本、可靠性等仅仅是利益关联方提出的诉求,不一定准确;如果经过分析有的约束没有必要,或成本太高、难度太大,这些约束是可以调整的。

    性能:需要达到 Kafka 的性能水平。

    成本:参考 XX 公司的设计方案,不超过 10 台服务器。

    时间:期望 3 个月内上线第一个版本,在两个业务尝试使用。

    可靠性:按照业务的要求,消息队列系统的可靠性需要达到 99.99%。

    安全性:消息队列系统仅在生产环境内网使用,无需考虑网络安全;如消息中有敏感信息,消息发送方需要自行进行加密,消息队列系统本身不考虑通用的加密。

    合规性:消息队列系统需要按照公司目前的 DevOps 规范进行开发。

    技术性:目前团队主要研发人员是 Java,最好用 Java 开发。

    兼容性:之前没有类似系统,无需考虑兼容性。

    2.3 复杂度分析

    [分析需求的复杂度,复杂度常见的有高可用、高性能、可扩展等,具体分析方法]

    子主题注:文档的内容省略了分析过程,实际操作的时候每个约束和限制都要有详细的逻辑推导,避免完全拍脑袋式决策。

    2.3.1 高可用

    对于微博子系统来说,如果消息丢了,导致没有审核,然后触犯了国家法律法规,则是非常严重的事情;对于等级子系统来说,如果用户达到相应等级后,系统没有给他奖品和专属服务,则 VIP 用户会很不满意,导致用户流失从而损失收入,虽然也比较关键,但没有审核子系统丢消息那么严重。综合来看,消息队列需要高可用性,包括消息写入、消息存储、消息读取都需要保证高可用性。

    2.3.2 高性能

    微博系统用户每天发送 1000 万条微博,那么微博子系统一天会产生 1000 万条消息,平均一条消息有 10 个子系统读取,那么其他子系统读取的消息大约是 1 亿次。将数据按照秒来计算,一天内平均每秒写入消息数为 115 条,每秒读取的消息数是 1150 条;再考虑系统的读写并不是完全平均的,设计的目标应该以峰值来计算。峰值一般取平均值的 3 倍,那么消息队列系统的 TPS 是 345,QPS 是 3450,考虑一定的性能余量。由于现在的基数较低,为了预留一定的系统容量应对后续业务的发展,我们将设计目标设定为峰值的 4 倍,因此最终的性能要求是:TPS 为 1380,QPS 为 13800。TPS 为 1380 并不高,但 QPS 为 13800 已经比较高了,因此高性能读取是复杂度之一。

    2.3.3 可扩展

    消息队列的功能很明确,基本无须扩展,因此可扩展性不是这个消息队列的关键复杂度。

    2.4 备选方案

    [备选方案设计,至少 3 个备选方案,每个备选方案需要描述关键的实现,无须描述具体的实现细节。此处省略具体方案描述]

    2.4.1 备选方案 1:采用开源的 Kafka

    Kafka 是成熟的开源消息队列方案,功能强大,性能非常高,而且已经比较成熟,很多大公司都在使用。

    2.4.2 备选方案 2:集群 + MySQL 存储

    首先考虑单服务器高性能。高性能消息读取属于“计算高可用”的范畴,单服务器高性能备选方案有很多种。考虑到团队的开发语言是 Java,虽然有人觉得 C/C++ 语言更加适合写高性能的中间件系统,但架构师综合来看,认为无须为了语言的性能优势而让整个团队切换语言,消息队列系统继续用 Java 开发。由于 Netty 是 Java 领域成熟的高性能网络库,因此架构师选择基于 Netty 开发消息队列系统。

    由于系统设计的 QPS 是 13800,即使单机采用 Netty 来构建高性能系统,单台服务器支撑这么高的 QPS 还是有很大风险的,因此架构师选择采取集群方式来满足高性能消息读取,集群的负载均衡算法采用简单的轮询即可。

    同理,“高可用写入”和“高性能读取”一样,可以采取集群的方式来满足。因为消息只要写入集群中一台服务器就算成功写入,因此“高可用写入”的集群分配算法和“高性能读取”也一样采用轮询,即正常情况下,客户端将消息依次写入不同的服务器;某台服务器异常的情况下,客户端直接将消息写入下一台正常的服务器即可。

    整个系统中最复杂的是“高可用存储”和“高可用读取”,“高可用存储”要求已经写入的消息在单台服务器宕机的情况下不丢失;“高可用读取”要求已经写入的消息在单台服务器宕机的情况下可以继续读取。架构师第一时间想到的就是可以利用 MySQL 的主备复制功能来达到“高可用存储“的目的,通过服务器的主备方案来达到“高可用读取”的目的。

    具体方案:
    在这里插入图片描述

    采用数据分散集群的架构,集群中的服务器进行分组,每个分组存储一部分消息数据。

    每个分组包含一台主 MySQL 和一台备 MySQL,分组内主备数据复制,分组间数据不同步。

    正常情况下,分组内的主服务器对外提供消息写入和消息读取服务,备服务器不对外提供服务;主服务器宕机的情况下,备服务器对外提供消息读取的服务。

    客户端采取轮询的策略写入和读取消息。

    2.4.3 备选方案 3:集群 + 自研存储

    在备选方案 2 的基础上,将 MySQL 存储替换为自研实现存储方案,因为 MySQL 的关系型数据库的特点并不是很契合消息队列的数据特点,参考 Kafka 的做法,可以自己实现一套文件存储和复制方案(此处省略具体的方案描述,实际设计时需要给出方案)。

    可以看出,高性能消息读取单机系统设计这部分时并没有多个备选方案可选,备选方案 2 和备选方案 3 都采取基于 Netty 的网络库,用 Java 语言开发,原因就在于团队的 Java 背景约束了备选的范围。通常情况下,成熟的团队不会轻易改变技术栈,反而是新成立的技术团队更加倾向于采用新技术。

    上面简单地给出了 3 个备选方案用来示范如何操作,实践中要比上述方案复杂一些。架构师的技术储备越丰富、经验越多,备选方案也会更多,从而才能更好地设计备选方案。例如,开源方案选择可能就包括 Kafka、ActiveMQ、RabbitMQ;集群方案的存储既可以考虑用 MySQL,也可以考虑用 HBase,还可以考虑用 Redis 与 MySQL 结合等;自研文件系统也可以有多个,可以参考 Kafka,也可以参考 LevelDB,还可以参考 HBase 等。限于篇幅,这里就不一一展开了。

    2.5 备选方案评估

    2.5.1 备选方案 1:采用开源 Kafka 方案

    业务主管倾向于采用 Kafka 方案,因为 Kafka 已经比较成熟,各个业务团队或多或少都了解过 Kafka。

    中间件团队部分研发人员也支持使用 Kafka,因为使用 Kafka 能节省大量的开发投入;但部分人员认为 Kafka 可能并不适合我们的业务场景,因为 Kafka 的设计目的是为了支撑大容量的日志消息传输,而我们的消息队列是为了业务数据的可靠传输。

    运维代表提出了强烈的反对意见:首先,Kafka 是 Scala 语言编写的,运维团队没有维护 Scala 语言开发的系统的经验,出问题后很难快速处理;其次,目前运维团队已经有一套成熟的运维体系,包括部署、监控、应急等,使用 Kafka 无法融入这套体系,需要单独投入运维人力。

    测试代表也倾向于引入 Kafka,因为 Kafka 比较成熟,无须太多测试投入。

    2.5.2 备选方案 2:集群 + MySQL 存储

    中间件团队的研发人员认为这个方案比较简单,但部分研发人员对于这个方案的性能持怀疑态度,毕竟使用 MySQL 来存储消息数据,性能肯定不如使用文件系统;并且有的研发人员担心做这样的方案是否会影响中间件团队的技术声誉,毕竟用 MySQL 来做消息队列,看起来比较“土”、比较另类。

    运维代表赞同这个方案,因为这个方案可以融入到现有的运维体系中,而且使用 MySQL 存储数据,可靠性有保证,运维团队也有丰富的 MySQL 运维经验;但运维团队认为这个方案的成本比较高,一个数据分组就需要 4 台机器(2 台服务器 + 2 台数据库)。

    测试代表认为这个方案测试人力投入较大,包括功能测试、性能测试、可靠性测试等都需要大量地投入人力。

    业务主管对这个方案既不肯定也不否定,因为反正都不是业务团队来投入人力来开发,系统维护也是中间件团队负责,对业务团队来说,只要保证消息队列系统稳定和可靠即可。

    2.5.3 备选方案 3:集群 + 自研存储系统

    中间件团队部分研发人员认为这是一个很好的方案,既能够展现中间件团队的技术实力,性能上相比 MySQL 也要高;但另外的研发人员认为这个方案复杂度太高,按照目前的团队人力和技术实力,要做到稳定可靠的存储系统,需要耗时较长的迭代,这个过程中消息队列系统可能因为存储出现严重问题,例如文件损坏导致丢失大量数据。

    运维代表不太赞成这个方案,因为运维之前遇到过几次类似的存储系统故障导致数据丢失的问题,损失惨重。例如,MongoDB 丢数据、Tokyo Tyrant 丢数据无法恢复等。运维团队并不相信目前的中间件团队的技术实力足以支撑自己研发一个存储系统(这让中间件团队的人员感觉有点不爽)。

    测试代表赞同运维代表的意见,并且自研存储系统的测试难度也很高,投入也很大。

    业务主管对自研存储系统也持保留意见,因为从历史经验来看,新系统上线肯定有 bug,而存储系统出 bug 是最严重的,一旦出 bug 导致大量消息丢失,对系统的影响会严重。

    2.5.4 360度环表

    在这里插入图片描述

    最终选择备选方案 2:

    排除备选方案 1 的主要原因是可运维性,因为再成熟的系统,上线后都可能出问题,如果出问题无法快速解决,则无法满足业务的需求;并且 Kafka 的主要设计目标是高性能日志传输,而我们的消息队列设计的主要目标是业务消息的可靠传输。

    排除备选方案 3 的主要原因是复杂度,目前团队技术实力和人员规模(总共 6 人,还有其他中间件系统需要开发和维护)无法支撑自研存储系统(参考架构设计原则 2:简单原则)。

    备选方案 2 的优点就是复杂度不高,也可以很好地融入现有运维体系,可靠性也有保障。

    备选方案 2 的缺点:

    备选方案 2 的第一个缺点是性能,业务目前需要的性能并不是非常高,方案 2 能够满足,即使后面性能需求增加,方案 2 的数据分组方案也能够平行扩展进行支撑(参考架构设计原则 3:演化原则)

    备选方案 2 的第二个缺点是成本,一个分组就需要 4 台机器,支撑目前的业务需求可能需要 12 台服务器,但实际上备机(包括服务器和数据库)主要用作备份,可以和其他系统并行部署在同一台机器上。

    备选方案 2 的第三个缺点是技术上看起来并不很优越,但我们的设计目的不是为了证明自己(参考架构设计原则 1:合适原则),而是更快更好地满足业务需求。

    三.架构设计模板

    [备选方案评估后会选择一个方案落地实施,架构设计文档就是用来详细描述细化方案的]

    3.1 总体方案

    [总体方案需要从整体上描述方案的结构,其核心内容就是架构图,以及针对架构图的描述,包括模块或者子系统的职责描述、核心流程]

    3.2 架构总览

    [架构总览给出架构图以及架构的描述]
    在这里插入图片描述

    架构关键设计点:

    • 采用数据分散集群的架构,集群中的服务器进行分组,每个分组存储一部分消息数据。
    • 每个分组包含一台主 MySQL 和一台备 MySQL,分组内主备数据复制,分组间数据不同步。
    • 正常情况下,分组内的主服务器对外提供消息写入和消息读取服务,备服务器不对外提供服务;主服务器宕机的情况下,备服务器对外提供消息读取的服务。
    • 客户端采取轮询的策略写入和读取消息。

    3.3 核心流程

    3.3.1 消息发送流程

    [此处省略流程描述]

    3.3.2 消息读取流程

    [此处省略流程描述]

    3.4 详细设计

    [详细设计需要描述具体的实现细节,跟随实际情况增删]

    3.4.1高可用设计

    消息发送可靠性:

    业务服务器中嵌入消息队列系统提供的 SDK,SDK 支持轮询发送消息,当某个分组的主服务器无法发送消息时,SDK 挑选下一个分组主服务器重发消息,依次尝试所有主服务器直到发送成功;如果全部主服务器都无法发送,SDK 可以缓存消息,也可以直接丢弃消息,具体策略可以在启动 SDK 的时候通过配置指定。

    如果 SDK 缓存了一些消息未发送,此时恰好业务服务器又重启,则所有缓存的消息将永久丢失,这种情况 SDK 不做处理,业务方需要针对某些非常关键的消息自己实现永久存储的功能。

    消息存储可靠性:

    消息存储在 MySQL 中,每个分组有一主一备两台 MySQL 服务器,MySQL 服务器之间复制消息以保证消息存储高可用。如果主备间出现复制延迟,恰好此时 MySQL 主服务器宕机导致数据无法恢复,则部分消息会永久丢失,这种情况不做针对性设计,DBA 需要对主备间的复制延迟进行监控,当复制延迟超过 30 秒的时候需要及时告警并进行处理。

    消息读取可靠性

    每个分组有一主一备两台服务器,主服务器支持发送和读取消息,备服务器只支持读取消息,当主服务器正常的时候备服务器不对外提供服务,只有备服务器判断主服务器故障的时候才对外提供消息读取服务。

    每个分组有一主一备两台服务器,主服务器支持发送和读取消息,备服务器只支持读取消息,当主服务器正常的时候备服务器不对外提供服务,只有备服务器判断主服务器故障的时候才对外提供消息读取服务。

    3.4.2 高性能设计

    3.4.3 可扩展设计

    3.4.4 安全设计

    消息队列系统需要提供权限控制功能,权限控制包括两部分:身份识别和队列权限控制。

    3.4.5 其它设计

    [其他设计包括上述以外的其他设计考虑点,例如指定开发语言、符合公司的某些标准等,如果篇幅较长,也可以独立进行描述]

    消息队列系统需要接入公司已有的运维平台,通过运维平台发布和部署。

    消息队列系统需要输出日志给公司已有的监控平台,通过监控平台监控消息队列系统的健康状态,包括发送消息的数量、发送消息的大小、积压消息的数量等,详细监控指标在后续设计方案中列出。

    3.4.6 部署方案

    [部署方案主要包括硬件要求、服务器部署方式、组网方式等]

    消息队列系统的服务器和数据库服务器采取混布的方式部署,即:一台服务器上,部署同一分组的主服务器和主 MySQL,或者备服务器和备 MySQL。因为消息队列服务器主要是 CPU 密集型,而 MySQL 是磁盘密集型的,所以两者混布互相影响的几率不大。

    硬件的基本要求:32 核 48G 内存 512G SSD 硬盘,考虑到消息队列系统动态扩容的需求不高,且对性能要求较高,因此需要使用物理服务器,不采用虚拟机。

    3.5 架构演进规划

    [通常情况下,规划和设计的需求比较完善,但如果一次性全部做完,项目周期可能会很长,因此可以采取分阶段实施,即:第一期做什么、第二期做什么,以此类推]

    整个消息队列系统分三期实现:

    第一期:实现消息发送、权限控制功能,预计时间 3 个月。

    第二期:实现消息读取功能,预计时间 1 个月。

    第三期:实现主备基于 ZooKeeper 切换的功能,预计时间 2 周。

    更多相关内容
  • 秀米微信图文编辑器如何复制

    千次阅读 2021-06-28 08:18:04
    秀米编辑器是一款非常好用的图文编辑器,在这里用户能够享受到更好的文章排版功能,多个免费的模板供用户使用,超级方便,今天小编就来教大家如何将排好版的文章给复制出来。2.我们打开秀米编辑器的网页...

    原创秀米微信图文编辑器如何复制?

    编辑:小鑫 来源:PC下载网时间:2018-02-19 07:19:38

    秀米编辑器里面编辑好的文章排版怎么才能够复制到其他编辑器中?小编教大家如何复制秀米里面的内容。

    方法步骤

    1.秀米编辑器是一款非常好用的图文编辑器,在这里用户能够享受到更好的文章排版功能,多个免费的模板供用户使用,超级方便,今天小编就来教大家如何将排好版的文章给复制出来。

    9216d0ee6dd624bc249f2114b6751278.png

    2.我们打开秀米编辑器的网页,在里面我们点击图文消息排版助手,然后进入到编辑器里面。

    ac78f24ea8349c34ed1036011a68de2f.png

    3.进入到编辑器里面之后,在左侧就能够看到编辑器给我们提供的模板了,点击一下就会在右侧编辑区域生成一个一样的模板。

    baf8d98923fc662b36917b9e44b6233a.png

    4.我们在里面还能够找到多种不同的模板,建议大家都去注册一个秀米的账号,注册之后就能够获得更多的免费模板使用了。

    001b64fed9325be69e75ec5118d35631.png

    5.想要复制的话,也是十分简单,我们在编辑器的上面会看到有一个对号的标识,点击一下之后就会提示我们可以使用复制或者ctrl+c来进行复制,然后直接粘贴到想要的其他编辑器中即可。

    002c5797cc2a65a66325539e6373e4c9.png

    小编总结

    以上就是秀米编辑器复制编辑好的文章的教程了,小伙伴们赶紧去试试吧。应该比你直接在微信里面编辑要美观的多哦。

    END

    展开全文
  • 用于分析和传播的可复制模板复制模板的Coursera课程该课程由埃默里大学的Melinda Higgins教授提供。 本课程将帮助您重新创建以前的同事完成的工作,重新访问您一段时间前放弃的项目,或仅以一致的格式和工作流程...
  • POI复制Excel模板并填充数据

    千次阅读 2018-12-18 10:46:21
    //如果这行没有了,整个公式都不会有自动计算的效果的 sheet.setForceFormulaRecalculation(true); 如果是xls格式的,就改为:  POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(file))...

    ---------------------------------------------------------------------------------------------
    [版权申明:本文系作者原创,转载请注明出处] 
    文章出处:https://blog.csdn.net/sdksdk0/article/details/85060879

    作者:朱培      ID:sdksdk0     
    --------------------------------------------------------------------------------------------

    我们最近需要对系统加一个报表导出的功能,可以通过POI直接导出,导出后的excel文件需要支持在office里面修改数据后图表也会自动变换。方法一可以使用jfreechart+poi,但是这种方法生成的图表是一张图片,不能在office中自动修改;第二种方法是poi调用 office的宏,它需要调用自定义的.dll 文件,也需要在windows环境中,所以不适用。

    我这边采用的是自定义 excel 模版,然后在模版定义图表 。 然后通过POI改变图表数据区域的数据值。从而达到改变图表的目的。

    1、先准备一个excel 模版,里面把需要的数据写好,然后先自定义图表,例如图表样式如下:

    2、把数据区域的内容情况,数据清空之后如下:

     

    3、在javaee工程中引入poi的相关jar包,并把excel模板放入相应工程的WEB-INF目录下,名称换为temple.xlsx

    <dependency>
    		    <groupId>org.apache.poi</groupId>
    		     <artifactId>poi-ooxml</artifactId>
    		    <version>3.12</version>
    		</dependency>
    		 
    		<dependency>
    			<groupId>org.apache.poi</groupId>
    			<artifactId>poi</artifactId>
    			<version>3.12</version>
    		</dependency>
    		<dependency>
    			<groupId>org.apache.poi</groupId>
    			<artifactId>poi-ooxml-schemas</artifactId>
    			<version>3.12</version>
    		</dependency> 

    4、读取模板文件,这里读取的是xlsx方式的,

                //excel模板路径
    			File f = new File(this.getClass().getResource("/").getPath());
    		    String filePath = f+File.separator+"WEB-INF"+File.separator+"temple.xlsx";
    			File file = new File(filePath);
    			FileInputStream in =new FileInputStream(file);
    			//读取excel模板
    			XSSFWorkbook wb = new XSSFWorkbook(in);
    			//读取了模板内所有sheet内容
    			XSSFSheet sheet = wb.getSheetAt(0);
    			//如果这行没有了,整个公式都不会有自动计算的效果的
    			sheet.setForceFormulaRecalculation(true);

    如果是xls格式的,就改为:

                POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(file));
                //读取excel模板
                HSSFWorkbook wb = new HSSFWorkbook(fs);

               //读取了模板内所有sheet内容
                HSSFSheet sheet = wb.getSheetAt(0);

    5、找到相应的数据行,进行数据填充,要从0开始计数,excel中的第1行,读取的时候是从0开始的,例如我这边添加表头,填充历史客流、年龄等信息进去

    Calendar cal = Calendar.getInstance();
    			String publishTime = DateUtil.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
    			if(null==name){
    				sheet.getRow(1).getCell(0).setCellValue("景区:全部");
    			}else{
    				sheet.getRow(1).getCell(0).setCellValue("景区:"+name);
    			}
    			if(null==beginTime){
    				sheet.getRow(1).getCell(2).setCellValue("时间:全部");
    			}else{
    				sheet.getRow(1).getCell(2).setCellValue("时间:"+beginTime+"~"+endTime);
    			}
    			sheet.getRow(1).getCell(6).setCellValue("导出时间:"+publishTime);
    			
    			//历史客流
    			int t1=3;
    			Map<String, String> param = initParam(null,beginTime, endTime);
    			List<Map<String, Object>> daycountlist = dayCountService.selectHistoryDayCountList(null,null,null,name,param.get("beginTime"), param.get("endTime"),12);
    			for (Map<String, Object> map2 : daycountlist) {
                    for (Map.Entry<String, Object> m : map2.entrySet()) {
                        sheet.getRow(t1).getCell(1).setCellValue(m.getKey());
    				    sheet.getRow(t1).getCell(2).setCellValue(Integer.parseInt(m.getValue().toString()));
    				    t1++;
                    }
                }
    //性别
    			Map<String, Object> sexInfo = sexCountService.selectContrastsInfo(name, beginTime, endTime);
    			int s1=30;
    			for (Object v : sexInfo.values()) {
    				   sheet.getRow(s1).getCell(2).setCellValue(Double.parseDouble(v.toString())/100.0);
    				   s1++;
    			}
    			//年龄
    			Map<String, Object> ageInfo = ageCountService.selectContrastsInfo(name, beginTime, endTime);
    			int s2=33;
    			for (Object v : ageInfo.values()) {
    				   sheet.getRow(s2).getCell(2).setCellValue(Double.parseDouble(v.toString())/100.0);
    				   s2++;
    			}

    6、保存文件

    // 保存文件的路径
    			String realPath = "/mnt/app/tomcat/webapps/uploadfile/";
    			if(null==name){
    				name="全部";
    			}
    			String newFileName = "report-" +name+"-"+ DateUtil.getAllTime()+ ".xlsx";
    			// 判断路径是否存在
    			File dir = new File(realPath);
    			if (!dir.exists()) {
    				dir.mkdirs();
    			}
    			//修改模板内容导出新模板
    			FileOutputStream out = new FileOutputStream(realPath+newFileName);
    			wb.write(out);
    			out.close();

    7、将文件路径返回给前端,直接给前端一个文件的url链接,让他自己location.href跳转就可以拿到文件了

    map.put("url", "/uploadfile/"+newFileName);

    或者也可以使用response返回

    //返回文件给前端
    FileUtil.downloadFiles(response, realPath+newFileName);

    
    
     public static void downloadFiles(HttpServletResponse response,
                String filePath) {
            response.setContentType("application/octet-stream");
            response.setCharacterEncoding("UTF-8");
            FileInputStream fs = null;
            BufferedInputStream buff = null;
            OutputStream myout = null;
    
            try {
                File file = new File(filePath.trim());
                if (file.exists()) {
                    String fileName = file.getName();
                    fs = new FileInputStream(file);
                    response.addHeader(
                            "Content-Disposition",
                            "attachment;filename="
                                    + URLEncoder.encode(fileName, "UTF-8"));
                    buff = new BufferedInputStream(fs);
                    byte[] b = new byte[1024];
                    long k = 0;
                    myout = response.getOutputStream();
                    while (k < file.length()) {
                        int j = buff.read(b, 0, 1024);
                        k += j;
                        myout.write(b, 0, j);
                    }
                    buff.close();
                } else {
                    PrintWriter os = response.getWriter();
                    os.write("文件不存在");
                    os.close();
                }
                if (myout != null) {
                    myout.flush();
                    myout.close();
                }
                if (fs != null) {
                    fs.close();
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (myout != null) {
                    try {
                        myout.flush();
                        myout.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }

    8、导出后效果如下,而且手动在excel中修改数据,图表可以自动变换

     

     

     

    展开全文
  • Node.js的Docker模板 该项目使用从Node.js 项目构建的官方Docker映像,并提供有关... 全部文件: 使用“克隆或下载”按钮,然后选择“下载ZIP”以复制整个模板集,并将其复制到您的项目中。 单个文件: 使用“ wget
  • 设计思路: 在Table的thead中加入一行隐藏的行,当要添加一行时,复制一次隐藏的行,再将实例数据填充到这一行中相应的位置,填充完毕之后,再将这一行加到表格中去,整个操作完成。 模板:隐藏的行即相当于模板,当...
  • 网络上的根据模板填充Word我都看过一些, 它们的功能在数据换行的时候用的是run对象的.addCarriageReturn()方法,或者是直接用\n实现换行。这些都不符合我的需求, 因为我要的是分段,而不是换行。换行的word导致另一...
  • FullscreenTheme:一个主题,它将渲染画布缩放到最大宽度和高度,以填充整个浏览器窗口。 安装 在您的Unity项目Assets目录中创建一个名称为"WebGLTemplates"的新文件夹 将您要使用的主题从该目录的文件夹"dist"复制...
  • C++——模板(超详细的模板解析)

    千次阅读 2022-03-28 17:29:35
    超详细的C++模板知识列表,持续维护。

    模板是一个非常强大的C++功能,STL的各种组件也是基于模板的。所以,无论是写程序了,还是读程序,都有必要了解一下C++的模板。

    关于什么是模板或者模板的基本定义,这里就不讲述了,本篇文章主要罗列出在使用模板过程中的一些问题和模板一些令人头疼的语法,并配合简单的demo,如果你只是希望查阅语法或者了解一些知识点,这篇文章可能会帮到你。

    声明:使用了using namespace std。对于应该包含进来的头文件,不再显示的声明。文中所有demo均经过测试。本文章基于《C++ Primer Plus》和《C++ Prime》。

    目录

    模板的基本声明和定义

    模板的声明

    定义一个模板函数

    定义一个模板类

    模板参数作用域

    关于模板工作原理

    非类型模板参数

    inline和constexp

     在模板类中使用模板类

     友元与模板类

    默认模板实参

    : : 二义性的解决

    类模板成员函数

    类模板的成员模板 

    实例化优化 

    类型转换和参数推断

    返回值类型推断

    兼容类型的模板问题

    函数指针实参推断

    模板实参推断

    左值引用

     右值引用

    引用折叠

    std::move

    短小精悍的std::move定义

    std::move的解析

    模板函数匹配的特殊性

    注意重载模板声明顺序 

    模板特例化

    特例化和重载 的区别

    类模板特例化 

    部分模板特例化

    特例化成员

    可变参数模板

    可变模板参数的具体作用

    模板技巧

    转发 

    使用std::forward 

    转发参数包 

    make_shared的工作原理


    模板的基本声明和定义

    模板的声明

    template <typename T>  int compare (T t1, T t2);
    template <typename T> class compare;

    定义一个模板函数

    template <typename T>
    int compare(T & t1, T & t2)
    {
        if(t1 > t2) 
            return 1;
        if(t1 == t2)
            return 0;
        if(t1 < t2)
            return -1;
    }

    定义一个模板类

    template <typename T>
    class compare
    {
    private:
        T _val;
    public:
        explicit compare(T & val) : _val(val) { }
        explicit compare(T && val) : _val(val) { }
        bool operator==(T & t)
        {
            return _val == t;
        }
    };

    模板参数作用域

    就如同其他的函数参数一样,或者是变量一样,就是普通的作用域规则。

    using T = int;
    T a = 10;
    template <typename T> class A;

     模板声明里的T不是上面的int而是模板参数。

    template <typename T> class A
    {
        U val; //error
        template<typename U> class B;
    };

    关于模板工作原理

    模板定义并不是真正的定义了一个函数或者类,而是编译器根据程序员缩写的模板和形参来自己写出一个对应版本的定义,这个过程叫做模板实例化。编译器成成的版本通常被称为模板的实例。编译器为程序员生成对应版本的具体过程。类似宏替换。

    模板类在没有调用之前是不会生成代码的。

    由于编译器并不会直接编译模板本身,所以模板的定义通常放在头文件中。

    非类型模板参数

    顾名思义,模板参数不是一个类型而是一个具体的值——这个值是常量表达式。

    当一个模板被实例化时,,非类型参数被一个用户提供的或者编译器推断出的值所代替。正因为模板在编译阶段编译器为我们生成一个对应的版本,所以其值应该能够编译时确定,那么他应该是一个常量或者常量表达式。

    有一句话说:C++的强大在于他的编译器强大,下面这个例子就是很好的说明。

    template <size_t N, size_t M>
    int str_compare(const char (&str1)[N], const char (&str2)[M])
    {
        return strcmp(str1,str2);
    }

     使用方法

    str_compare("hello","nihao")

    为什么???我们甚至没有用<>来传递模板参数。这是因为编译器在编译阶段已经帮助我们计算好了应该开辟多大空间的数组。我们也可以指定长度。N,M只是隐式的传入进去。

    编译器也可以自动帮助我们推断参数时什么类型,从而不用显示的调用模板函数,对于上面的compare函数,我们可以这样调用,前提时保证参数类型相同。

    compare(10,20);

    非类型模板参数的范围

    整形,指针或者左值引用都是一个非类型模板参数。

    我们可以想到,对于指针或者引用,应当保证实参必须具有静态的生存期,保证其不会被释放。

    inline和constexp

    放在模板之后,函数之前即可

    template <typename T>
    inline int compare(T t1, T  t2);

     在模板类中使用模板类

    这个应该很好理解,根据自己的需求,我们可以这样定义

    template <typename T>
    class A
    {
    private:
        vector<T> vec;
    };

     也可以这样定义

    template <typename T>
    class B
    {
    private:
        vector<int> vec;
    };

     友元与模板类

    通过上面编译器为模板生成具体代码的原理可以看出这样有什么不同

    template <typename N>  
    class C
      friend A<N>;
      friend B<int>;

     由于具体的原理类似宏替换,每个对应的C<N>都有友元A<N>和B<int>、

    即有这样友元关系C<int> A<int> B<int>, C<string> A<string> B<string>以此类推。

    还有这样的模板友元——所有的实例化都是其友元

    template <typename N>
    class C
        template <typename T> friend class D;

     但是没有这样的写法

    template <typename T> friend class D<T>;

    或者这样的写法

    template <typename T> friend D<T>;

    模板允许模板参数为自己的友元

    首先说明,模板允许内置类型为自己的友元。

    friend int;

    这样写是完全正确的,但是实际上有什么意义呢?

    还是有意义的,我们可以这样写

    template <typename T>
    class People
    {
        friend T;
    };

    这样就保证了在传入内置类型的时候不会有错误。

    默认模板实参

    用法和函数的默认参数基本相同

    template <typename T = int> class A;

    默认的情况下T就是int

    A<> a; // T is int

    : : 二义性的解决

    对于普通类的:: ,我们可以知道它究竟是一个类还是一个静态成员,就像下面这样。

    string::size_type a;
    string::npos;

    对于模板类来说,我们还是知道表达的是什么,但是已经说过了,模板类在没有 调用之前不会生成代码,这可坏了。对于T::mem,究竟是什么呢?是静态成员?还是一个类型的typedef?

    对于这个问题,使用typename修饰。

    当我们希望通知编译器一个名字表示一个类型时,使用且必须使用关键字typename,来表示其是一个类型。

    于是,我们可以写出这样的代码。

    template <typename T>
    typename T::val_typefunc ();

    表的不是一个静态数据成员而是一个类型。

    或者这样的代码

    typedef typename T::mem s_type;

    表示s_type是一个类型的别名而不是数据成员的别名。

    如果转到string::size_type的定义,可以看见他是一个typename 的 typedef。

    类模板成员函数

    本质上就是个函数,只要掌握了模板的工作原理,我们我们就可以轻松的写出类模板成员函数。

    class Math
    {
    public:
        template <typename N> inline static  N sqrt(N);
    };
    
    template<typename N>
    N Math::sqrt(N val)
    {
        return val * val;
    }

    首先来一点一点解析

    这是一个模板函数,返回值为N类型,所以,模板语法写在前面,让编译器知道应该返回类型,紧接着就是返回类型,返回类型同上都是写在比较靠前的位置。接着就是函数的标签。

    对于定义来说,应该知道是哪个类下的函数,所以和普通的方法一样加上一个作用域即可。

    假如把类写成这样呢?

    template <typename N>
    class Math
    {
    public:
        inline static  N sqrt(N);
    };
    

    那方法的定义应该是写成这样的。

    template<typename N>
    N Math<N>::sqrt(N val) 
    {
        return val * val;
    }

    这里就可以看出

    前面说到的,模板不是一个具体的类,而是根据这个模板编译器生成对应的版本。

    对于每一个版本,都是不同的类。就像重载函数一样,即便参数个数和函数的具体算法完全一样,但类型不同他们也是不同的函数,只不过函数名相同而已。

    那么就应该可以得到每个版本的类都对应的一个相应版本的静态成员。所以Math<N>::这样写也就很好理解了。

    类模板的成员模板 

    我已经不知道用什么语言来下面的代码了。但是我们知道了一些事情。

    无论是定义还是声明,模板语法的优先级是最高的,不同模板的优先级又根据其声明顺序来判断,其次是函数修饰,然后是返回值。根据这个原则我们可以轻松的解析这个函数。 

    template <typename T> class A
    {
    public:
        template <typename It>  A<T> sum(It _begin, It _end);
    };
    
    template <typename T>        //最外层模板
    template <typename It>       //内层模板
    A<T>                         //返回值
    A<T>::sum(It _begin, It _end)//函数标签 
    {}                           //算法实现
    
    //不妨写的更美观一点
    template <typename T>
      template <typename It>
      A<T> A<T>::sum(It _begin, It _end){
          
      }

    注意:上面的代码和下面的代码写的足够复杂,下面的代码对其进行一些小小的修改。 

    具体的用法,虽然下面的例子看起来有些造作,但是还是能说明一些问题的

    #include <iostream>
    #include <cstring>
    #include <vector>
    using namespace std;
    template <typename T> class A
    {
    private:
        vector<T> vec;
    public:
        template <typename It>  T sum(It _begin, It _end);
        A(initializer_list<T> initlist)    
        {
            for(auto it = initlist.begin();it != initlist.end();++it)
            {
                vec.push_back(*it);
            }
        }
        typename vector<T>::iterator begin()
        {
            return vec.begin();
        }
        typename vector<T>::iterator end()
        {
            return vec.end();
        }
    };
    
    template <typename T>
      template <typename It>
      T A<T>::sum(It _begin, It _end)
      {
        T tot ;
        memset(&tot,0,sizeof (T));
        while(_begin != _end)
        {
            tot += *_begin++;
        }
        return tot;
      }
    
    
    int main()
    {
        A<int> a {1,2,3,4};
        cout << a.sum(a.begin(),a.end());
        return 0;
    }

     虽然这样的语法很是令人头疼,但是多用即可熟练,或者说使用类型别名来避免这样的问题,并且最好不要把学习精力放在语法上——在没有熟悉语法之前。

    实例化优化 

    当模板被调用时才会被编译,那么就会存在这样一种情况——相同地实例化可能出现在多个文件对象中。当两个或多个独立编译地源文件适用了相同地模板,并提供了相同地模板参数时,每个文件中就都会有该模板的一个实例。

    为了解决这种问题,我们可以控制显示实例化。具体的做法如下

    用关键字extern显示的实例化声明

    extern template class A<string>;                   //声明
    template int compare(const int &, const int &);    //定义

    将一个实例化声明为extern就表示承诺在程序的其他位置有该实例化的一个非extern声明(定义)。

    由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何用此实例化版本的代码之前。 

    解释的来说:因为文件其他处已将有一个实例化——编译器生成好的或者是自己定义的,由于编译单元为.cpp文件,所以在一个文件中实例号的代码并不能用于另一个文件,这就显着很捞。而extern正是解决这个问题的。 

    类型转换和参数推断

    与非普通地类型转换一样,模板类在传递模板参数的时候也会进行相应的转换,只不过这种转换增添了更多的规则,参数推断和类型转换的关系是非常紧密的。

    类型转换这里的问题如果想要清楚的了解,那恐怕是非常可怕的,我有时候在想,通过这么多的转换规则 ,我们就可见一斑的看出C++的设计是多么的巧妙。虽然这的知识点很乱,但其实只要抓住隐藏在这背后的观念就能清晰的对付各种转换了。

    其中一个规则是:如果能够进行安全转换,那么编译器可以隐式转换

    最经典的一个例子就是non-const  到const的转换。

    为了展示的方便,会忽略掉一些代码

    template<typename T>
    bool func(const T t1, const T t2)
    {
        return less<T>()(t1,t2);
    }
    
    ...
    
        int a = 10;
        const int b = 20;
        func<int>(a,b);

    这里的int转为cosnt int是允许的,因为这是按值传递,并且non-const 转为 cosnt也不会带来什么坏处。因此,编译器会执行这样的转换。

    我们不妨修改一下这个函数

    bool func(const T & t1, const T & t2);
    bool func(T & t1, T & t2);

    上述第一个声明会正常调用——虽然是引用,和上面的是同样的道理。

    而第二个却不会正常调用——因为b是一个cosnt 将要转换为non-const,我中转换是不安全的,所以编译不允许这样的转换。

    假设我们这样调用两个函数

    func<int>(10,20);

    同样的,对于第一个是允许的——虽然讲右值绑定到左值引用上,但是我们用const修饰形参,保证其不会改变,所以编译器同意这样的转换。

    而对于第二个,编译器则不允许这样的转换,因为我们的形参是non-const的,不能保证不修改形参的值,形参正好又是一个引用,这样可以修改实参的值——恰好实参是一个右值——是不允许被修改的,所以编译器不允许这样的转换。

    基于上面的转换规则,我们可以知道,如果函数形参不是引用类型,则可以对数组或者函数类型的实参应用正常的指针转换。

    上面的是C++ Primer的原文,实际上笔者在学习的过程中,发现了其错误。

    先来看一下代码

    template <typename T> bool func(const T & t1, const T & t2);
    ...
    int a[10];
    int b[10];
    func(a,b);

    这样是可以的。那这样呢?

    template <typename T> T func(const T & t1, const T & t2);
    template<typename T1, typename T2, typename T3>
    T1 sum(T2 t2, T3 t3)
    {
        return t2 + t3;
    }
    
    ...
    
    sum<long long>(10,200); //or sum<long long, int, int>();

    就不可以了,这个声明对应着C++ Primer的声明。

    为什么一样的形参列表只有返回值不同编译器就会发出警告,这是为什么。

    其实我们到目前为止的讨论,都适用于普通函数,模板的本质其实也是模板为我们生成对应的版本,为了解开上面的疑惑,我们可以先来复习以下引用的知识。

        int *& ref_apple_point = &apple; //error
        int * const & ref_apple_point_const = &apple; //ok

    根据这两行代码我们可以得到一些启示。

    因为数组名是一个常量,const T & t1这样的形参是可以接受的。但对于返回值来说,可就麻烦了。返回值为内置数据类型的模板函数,对于这个问题,这没有什么好说的。返回类型为T的模板函数,他返回的是一个什么具体类型呢?首先T被u推断为* const,那么返回类型也应该是 *const

     在这里我们先留下一个悬念,当我们理解和编译器是如何推断T是何种类型的时候,这个问题可能就会迎刃而解。

    返回值类型推断

    在编译器遇见函数列表之前,所有的形参都是不存在的,那么我们需要使用这样的尾置返回类型。

    auto func(It & _beg, It & _end) -> decltype(*_beg)
    {
        //...
        auto sum = *_beg;
        sum = 0;
        for_each(_beg,_end,[&sum](const int & val){ sum+= val;});
        //...
        return sum;
    }

    这样的代码还是有一些问题的,如果我们要返回一个拷贝而不是引用呢?要用到一个类型转换模板工具。

    remove_reference<>        移除引用——关于其他的类型转换,不再本文章讨论范围内读者可自行查阅。

    这个模板类有一个名为type的public成员,能够获得相应的类型。所以我们可以这样写

    template <typename It>
    auto func(It & _beg, It & _end) -> typename remove_reference<decltype(*_beg)>::type //don't forget typename
    {
        //...
        auto sum = *_beg;
        sum = 0;
        for_each(_beg,_end,[&sum](const int & val){ sum+= val;});
        //...
        return sum;
    }
    

    在某些情况下我们可以指定返回u类型,例如

    template<typename T1, typename T2, typename T3>
    T1 sum(T2 t2, T3 t3)
    {
        return t2 + t3;
    }
    
    sum<long long>(10,200); //or sum<long long, int, int>();

    显示模板参数按从左到右的顺序一次匹配。

    兼容类型的模板问题

    有这样的代码

    template<typename T>
    T sum(T t1, T t2)
    {
        return t1 + t2;
    }
    
    sum(10,3.14);

    虽然int和double兼容,但是只有一个类型参数,编译器傻了,T为int?精度会丢失,肯定是不可行的,T为double?貌似也不行,这样会导致数据溢出。无奈我们只好这样了。

    template<typename T1, typename T2>
    ??? sum(T1 t1, T2 t2)
    {
        return t1 + t2;
    }

    至于返回类型,全交给程序员来规定,或者用尾部返回类型。

    函数指针实参推断

    有趣的是,虽然在未实例化之前,编译器没有生成具体的代码,但我们仍然可以进行函数指针绑定的操作。

    template <typename T> int compare(const T & t1, const T & t2) { }
    
    int (*pf_int)(const int &,const int &) = compare;

    同样的我们也可以将模板函数作为回调函数进行传参,但此时可能会产生二义性,所以注意显示的写出模板参数。

    当参数是一个函数模板实例的地址时,程序上下文必须满足:对于每个模板参数,能唯一确定其类型的值。

    模板实参推断

    这里是重中之重!!!重中之重!!!

    很多的模板问题都与此有关。

    关于const和&的问题,我们上面已经讲过了。这里再进行进一步的说明。

    左值引用

    template <typename T> void func1(T &) { }
    template <typename T> void func2(const T &) { }
    void aa()
    {
        int a = 10;
        const int b = 20;
        func1(a);   //T is int 
        func1(b);      //T is const int
        func2(a);      //T is int
        func2(b);      //T is int
        func2(10);     //T is int 
    }

    还是比较有意思的,看func2(b)的调用,虽然我们将const int类型传入进去,但是编译器为我们推导的还是int,原因应该和参数类型有关,如果编译器为我们推导的是const int ,那么const const int是不合法的,所以只好为我们推倒为int,即使我们调用时候的类型是const int。

     右值引用

    template <typename T> void func(T &&);
    func(10);         //T is int
    func(b);          //b is a left_val T is ???

    我们可以根据引用折叠可以推断出类型。

    引用折叠和万能引用

    众所周知在非模板函数中可以使用const & 来接受任意类型参数,在模板中,也有类似这样的万能引用,就是&&。知道了这样的原因是有着引用折叠得的存在。

    先说结论:在传递参数的过程中,无论多么复杂的引用传参,最后都会被折叠为& 或者 &&.

    如果我们间接创建了一个引用的引用,则这些引用形成折叠。除了右值引用的右值引用会被折叠为一个右值引用,剩下全部折叠为一个左值引用。即

    T& &, T& &&, T&& &都会折叠为T&

    T&& &&会被折叠为&&

    这就意味着我们 可以解释上面的问题。

    当我们将一个左值传递给一个右值引时候,编译器推断T的类型为&。注意是T的类型为左值引用,不是整个形参是T &。

    所以

    func(b);          //b is a left_val T is int&

    上述的两个规则导致了

    如果一个函数参数是一个指向模板类型参数的右值引用,则他可以被绑定到一个左值。

    如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数将被参数将被实例化为一个普通左值引用参数。

     这两个规则又暗示了我们——我们可以将任意类型的实参传递给参数为右值引用的函数。

    当代码中涉及的类型可能是非引用类型,也可能是引用类型的时候,编写正确的代码就变得异常困难(虽然remove_reference这样的转换类型对我们可能有所帮助)。

    PS:由于这里的知识是在是很乱,笔者在写这里的时候也实在无能为力,所以大量了引用C++ Primer的原文。但是有一点可以保证——笔者在这里写的demo虽然没有什么实际意义仅用于演示——但是也能说明一些问题。

    如果读者对模板的细节想以探究经,可以翻越C++ Primer——中文第五版P508-P610。

    如果想巩固这里的语法,可以作相应的配套习题。

    std::move

    折磨的篇章终于过去了,让我们用好奇心来看一看std::move这个工具。

    短小精悍的std::move定义

    如下

    template <typename T>
    typename remove_reference<T>::type && move(T && t)
    {
        return static_cast<typename remove_reference<T>::type&&>(t); 
    }

    std::move的解析

    因为move可以接受任意对象,所以应当是一个模板类。

    既然我们要保证返回一个右值,那我们应当明确的得到一个非左右值引用类型——即普通类型。

    那么就可以先移除引用再加上右值引用——这样保证了返回一个右值引用对应了

    typename remove_reference<T>::type &&

    既然接受任意一个对象,那美可以用&&来接受实参,对应

    move(T && t)

    我们只需要返回一个右值即可,所以只有一个return语句。

    我们回想以下为什么要使用std::move——获得一个右值进行移动构造?又或者是仅仅需要一个右值?不管出于什么原因,最终的目的就是为了优化程序,所以通过形参创建一个额外的右值并返回这样是不可取的,是脱裤子放屁,所以我们要使用这条语句

    static_cast<typename remove_reference<T>::type&&>(t);

    通常情况下,static——cast用于合法的类型转换,但是又一种情况例外,虽然一个左值不能隐式转换为右值,但是可以使用static_cat将其显示的转换为右值——前提我们先移除对象身上的引用效果。

    模板和重载

    模板也是可以被重载的,只要没有二义性。像在C++库中,存在着大量的模板重载技术,或者是可变模板参数中,也存在着模板的重载。

    对于实例化的选择,遵循以下的规则。

    1.对于一个调用,其候选函数是所有可行的实例化

    2.可行函数按类型转换来排序。当然,可用于函数模板调用和的类型转换是非常有限的。

    3.和普通函数一样,如果恰又一个函数比任何其他函数都更好的匹配,则选择此函数。

    4.如果多个函数提供了同样好的匹配

            1)优先选择非模板函数

            2)没有非模板函数我选择更加特例化的模板

            3)否则有二意性

    正确的定义一组重载的函数模板需要对类型键的关系以及模板幻术允许的优先的实参类型转换有着深刻的理解。

    注意:虽然非模板函数的优先级很高——但那也是没有对应模板匹配的情况下,所以,在重载模板的时候仔细观察和思考。

    所以我们为了适配字符串的比较,可以写出这样的代码

    template<size_t N, size_t M>
    int compare(const char str1[N], const char str2[M])
    {
        return strcmp(str1,str2);
    }
    //或者
    int compare(const char * const str1, const char * const str2)
    {
        return strcmp(str1,str2);
    }

     根据上面的匹配规则,我们还可以递归的调用模板类自己实现某些功能

    template<typename T> string debug_rep(const T & t)
    {
        ostringstream ret;
        ret << t ;
        return ret.str();
    }
    
    template<typename T> string debug_rep(const T * p)
    {
        ostringstream ret;
        ret << "pointer :" << p;
        if(p != nullptr)
            ret << " " << debug_rep(*p);
        else
            ret << " nullptr";
        return ret.str();
    }
    
    //适配C风格字符串
    string debug_reo(char * p)
    {
        return debug_rep(string(p));    //这是一个右值,不能获取其地址
    }
    
    string debug_rep(const char * p)
    {
        return debug_rep(string(p));
    }
    
    int main()
    {
        string s("hello");
        string* ps = &s;
        cout << debug_rep(ps) << endl << debug_rep(s);
        return 0;
    }

    运行结果

    模板函数匹配的特殊性

    难道没有发现一个异常的地方吗?对于指针版本的调用,可以这样实例化两个函数

    string debug_rep(const string* & t);
    string debug_rep(string * p);

     对于普通函数,这是无疑的二义性,但是模板会选择特例化高的,原因是const T&可以实例化任何类型,而const T * p只能实例化指针类型——特例化程度更高。所以会调用后者。

    这就说明了:不要将普通函数的匹配机制应用于模板函数匹配机制——虽然两者很像,但是还是有某些地方是不一样的。

    注意重载模板声明顺序 

    由于模板的特性,使得其可以递归的调用自己的不同版本,但是注意要调用的版本一定要事先声明或者定义,否则可能出现函数不匹配的情况

    我们把适配char * 接口的字符串的函数放到最前面,我们发现编译器会右值河阳的错误。

    No matching function for call to 'debug_rep'

    调用“debug_rep”没有匹配的函数 

    或者我们将两个debug_rep的模板版本调换以下顺序。 

    Call to function 'debug_rep' that is neither visible in the template definition nor found by argument-dependent lookup

    调用函数“debug_rep”,该函数在模板定义中既不可见,也不通过参数相关查找找到

    模板特例化

    我们编写的模板,不可能保证对于所有的类型都能适用——compare函数就是很经典的例子,对于两个指针类型,仅仅是毫无意义的比较。这时候我们用到模板特例化的技术可以很好的解决这样的问题。

    由于less的底层是使用<来比较的,所以less并没有适配字符指针。那么,我们可以编写这样的模板特例化。

    template <>        //表示一个模板特例化——语法规定
    int compare(const char * const & str1, const char * const & str2) //具体的类型
    {
        return strcmp(str1,str2);
    }
    

    可以看出,模板特例化的尖括号中没有任何说明,所以模板特例化要对所有模板那参数都进行特例化。 

    注意,上面的特例化只能处理字符指针,不能处理数组或者字符串面量——这和函数匹配机制有关。这个特例化仅仅接受char*以及其const版本,虽然字符数组的名字就是他的地址,但是在模板中会被解释为一个字符数组的引用,更加的精准匹配。如果想要支持字符面量(本质上是字符数组)和字符数组,请写一个重载的模板函数——见模板重载。

    我们可以使用调试观察是如何推断实参类型的

    推断为一个数组的引用——这显然比将数组转换为指针再进行匹配更加精确。

    特例化和重载 的区别

    特例化就是一个特殊的实例化——模板的实例化,所以,特例化仅仅是模板的一个实例化,不会影响函数匹配。 

    并且,模板特例化一定要保证模板的之前的声明或者定义。如果不这样做——编译器不会报错,但是会有一些令人匪夷所思的地方。模板会由编译器实例化,而不是调用自己特例化版本——这种错误往往很难查找。所以,记住一个规则:特例化一个模板,一定要保证其在原模板的定义域中。

    类模板特例化 

     这里引用《C++ Primer》的例子——对其做一些解析。

    hash容器是能够进行十分快速的查找容器,像hash_map, hash_set等。他们的底层使用什么来映射哈希值呢? hash模板类。hash - C++ Reference (cplusplus.com)

    那么对于我们自定义的类型来说,没有其对应算法,为了能够使用我们的自定义类型,我们可以定义一个其特例化版本。

     一个hash的特例化必须包括

    一个重载的调用运算符,接受一个容器关键字类型的对象,返回一个size_t——用于映射对象的哈希值。

    两个类型成员,result_type, argument_type,分别调用运算符返回类型和参数类型。

    默认构造函数和拷贝赋值运算符。

    于是我们可以写出如下的代码。

    class Book
    {
        friend class std::hash<Book>;    //hash使用了私有成员,所以将其声明为友元
    private:
        int book_id;
        string book_name;
    public:
        Book() = default;
        Book(const int a, const string & _name) : book_id(a), book_name(_name) { }
        bool operator==(const Book & b) const
        {
            return book_id == b.book_id;
        }
    };

     我们首先定义一个Book类,然后提供其==运算符确保hash模板能够自持我们的自定义类型。

    随后我们在std命名空间中特例化一个和hash,

    namespace std
    {
        template <>
        struct hash<Book>
        {
            //必须提供的成员
            typedef size_t result_type;
            typedef Book argument_type;
            size_t operator()(const Book & b) const;
        };
        size_t
        hash<Book>::operator()(const Book &b) const {
            //自定义如何组织hash_val
            return hash<string>()(b.book_name) ^ hash<int>()(b.book_id);
        }
    }

    之后,我们就可以使用unordered_set/map,来操纵我们的自定义类型了。 

    为了能够让自定义数据类型的特例化能够被正常使用,应该将其放在类声明对应的头文件中,或者用别的头文件将其包含进来。

    部分模板特例化

    我们可以指定一部分而非所有的模板参数,或者是参数的一部分而非全部特性。一个模板的部分特例化本身是一个模板,使用它时用户还必须为哪些在特例化版本呢中未指定的模板参数提供实参。

    部分特例化一部分模板参数特例化,没有特例化的部分额外的提供实参。

    标准库的remove_reference 就是使用一系列的特例化完成其功能的,我们将其转到定义。

    这里部分特例化的时参数的引用性质。

      template<typename _Tp>
        struct remove_reference
        { typedef _Tp   type; };
    
      template<typename _Tp>
        struct remove_reference<_Tp&>
        { typedef _Tp   type; };
    
      template<typename _Tp>
        struct remove_reference<_Tp&&>
        { typedef _Tp   type; };

     具体的语法:

    在正常的模板声明之后,在类的后面使用尖括号<>放入要特例化的实参,这些实参于原始模板中的参数按照位置对应。也就是对应着上面源码中的

    struct remove_reference<_Tp&&>

    注意:模板部分特例化并不是对于某个单单的模板参数特例化,也可能是模板参数属性的特例化,这点对于理解类模板部分特例化十分重要

    注意:我们只能部分特例化类模板,而不能部分特例化模板函数。 

    例如我们定义一个泛用的模板类和实例化一个其专门用来处理指针的类

    
    template <typename T>
    class A;
    
    template <typename T>
    class A<T *>;

    特例化成员

    我们可以只特例化特定的成员函数而不是特例化整个模板类。

    template <typename T>
    class A
    {
    private:
        T val;
    public:
        A(const T & t) : val(t) { }
        void func();
    };
    
    template<>
    void A<int>::func()
    { }
    
    
    

     那我们这样得到的就是对于A<int>的实例化下的一个特例化func——这个func只在int的实例化版本生效。也就是说,实例化int版本的A其对应的func是我们特例化的这个版本,而其他成员还是正常的实例化。

    可变参数模板

    声明:关于模板的递归调用,都有一个基线函数,只不过是没有写出。

    关于可变参数模板,一部分引用我之前的文章,再在这里做一些补充

    可变模板参数 variadic template

    包                   packet

    模板参数包     template parameter packet

    函数参数包     function paremeter packet 

     C++——C++11的标准(下)___JAN__的博客-CSDN博客https://blog.csdn.net/JAN6055/article/details/122818685?spm=1001.2014.3001.5501

    详情见这篇文章的可变模板参数。 

    补充:

    sizeof...运算符

    能够获得包中参数的个数

    template<typename T, typename... Args>
    void var_fun(const T & t, const Args&... args)
    {
        //cout << t;
        cout << "element numbers of packs is " << sizeof...(Args);
        //var_fun(args...);
    }

    包拓展

    拓展        packs expand

    包拓展简单的来说将他分解为其构成的元素,如果说将参数变为包的过成类比为压缩未见,那么包拓展就是解压文件,但包拓展不仅仅是包展开。

    当拓展一个包时,我们还要提供用于,每个拓展元素的模式。拓展一个包就是将它费解为构成的原书,对每个元素应用模式,获得拓展后的列表。我们通过在模式右边防一个省略号...来触发拓展操作 。

    什么是模式?

    在实际生活中,当我们说以一种模式打开某个东西,或者是什么模式打开时。指定的是固有的模式,比如说性能模式,均衡模式等。而抱拓展的模式更像是对于每个元素都调用一次相应的函数,包拓展需要我们自定义模式——其实就是一个函数,返回值为包中的一个元素应用模式后的结果,所有这样的结果组合在一起,也就是包以这个模式(函数)展开。

    看一下标准库的配置器中是如何使用的展开

    noexcept(noexcept(::new((void *)__p)
    			    _Up(std::forward<_Args>(__args)...)))

    这是一个函数异常声明的部分,当用一个包构造一个元素的时候不会抛出异常,仅当,使用转发模式对参数包进行展开的时候不抛出异常。

        var_func(args...);      //默认的包展开
        //注释部分的...不为关键字,和C++语法没有任何关系
        //相当于这样{ele1, ele2, ele3, ... ,elen}
        var_fun(mul(2,args)...);    //带有模式的包展开
        //第二种展开模式相当于这样
        //{ mul(2,ele0),mul(2,ele1),mul(2,ele2), ... mul(2,elen) }

    具体实验

    template <typename T>
    void print(const T & t)
    {
      cout << t << endl;
    }
    template <typename T, typename... Args>
    void print(const T &t ,const Args... args)
    {
      cout << t << endl;
      print(args...);
    }
    
    template <typename T>
    int up(T & t)
    {
      t *= 2;
      return t;
    }
    template <typename... Args>
    void func(Args&&... args)
    {
      print(up(args)...);
    }
    
    int main()
    {
      func(1,2,3,4,5);
      return 0;
    }
    

     运行结果

    可变模板参数的具体作用

    可变模板参数可以说是一个核弹,比如tuple就是使用其实现的,模板类tuple以私有继承的方式继承它自己并结合模板部分特例化。如下

    template<typename T, typename ... Args>
    class tuple<T,Args...> : private tuple<Args...>
    {
        //something
    };

     还是很奇妙的,具体详情请观看侯捷老师的视频——bilibili : 

    模板技巧

    模板的功能还是很强大的,我们有必要学习一些模板技巧。

    转发 

     什么是转发?

    某些函数需要将其一个或多个实参连同类型不变的传递给其他函数。这个过程就叫转发。

    很形象,一个函数把数据原封不动的传递给另一个函数,就是转发。

    什么时候会用到转发呢?比如说我们有这样的一个函数,在容器尾部直接使用我们穿进来的参数构造一个元素,这个时候使用转发就是很有必要的。如果我们不适用转发技术,可能会造成变量的复制,也许有的时候这个函数能正常使用,但是有的时候我们就需要引用来做事,所以这样做留下的错误的隐患。 

    假设有func1(int &,args)  fun2 work(args,int&);

    我们需要传进func1一个整形,经过func2的中间媒介,传入work,并在work中改变那个变量。

    读者可以试一下func2中使用什么样的参数,经过怎样的变换可以对原来的参数的性质原封不动的传递给work。这是比较简单的情况了。STL的部分函数实现会有恐怖的调用层次,如果不使用转发技术后果可想而知。

    使用std::forward 

    要说转发一定离不开std::forward

    forward返回实参类型的右值引用。它和move很像,但前者是返回给定类型的右值引用,如果给定的类型是左值引用也返回其右值引用——左值引用,并且其必须显式的指定模板参数;而move无论模板参数类型是什么都返回一个右值引用(只能是右值引用),因为前面已经看到了move的实现方法。

    于是我们可以定义下面的转发函数        

    template <typename F, typename T1, typename T2>
    void fun(F f, T1 && t1, T2 && t2)
    {
        f(std::forward<T2>(t2), std::forward<T1>(t1));
    }

    使用右值引用作为模板参数——确保接受任意对象,并保证其能保持原来的性质不变(见引用折叠)。在发送参数的过程中获得对应类型的右值——确保其传递给函数的参数的性质不变(见引用折叠)。

    更简单的来说,上述的写法是对于所有类型的对象,无论进行何种参数传递,其参数的性质都不会改变的通用情况。

    转发参数包 

    根据上面转发的关键字,我们可以知道,在进行转发的时候应该以何种模式进行包展开。

    (Args&& ... args) //以右值引用展开参数包
    std::forward<Args>(args)... //将包中的每一个元素应用于forward

     所以我们可以这样做

    template<typename ... Args>
    void buffer_fun(Args &&... args)
    {
        work(std::forward<Args>(args)...);
    }

    make_shared的工作原理

    std::make_shred就是基于转发参数包实现的。

    让我们先来回忆make_shared的其中一个使用方法。

    make_shared<T> name (args);

    很明显的可以推断其应该使用用可变模板参数,我们转到其定义

      template<typename _Tp, typename... _Args>
        inline shared_ptr<_Tp>
        make_shared(_Args&&... __args)
        {
          typedef typename std::remove_cv<_Tp>::type _Tp_nc;
          return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
    				       std::forward<_Args>(__args)...);
        }

    可以看见,其使用部分特例化和可变参数模板,将包转发给了std::allocate_shared进行空间分配,我们进一步的转到std::allocate_shared中可以看见其有进一步的将其转发给了其他的模板

      template<typename _Tp, typename _Alloc, typename... _Args>
        inline shared_ptr<_Tp>
        allocate_shared(const _Alloc& __a, _Args&&... __args)
        {
          static_assert(!is_array<_Tp>::value, "make_shared<T[]> not supported");
    
          return shared_ptr<_Tp>(_Sp_alloc_shared_tag<_Alloc>{__a},
    			     std::forward<_Args>(__args)...);
        }

    至此,C++的模板的基础知识点大抵应该是都讲完了,如果日后有一些杂项补充的话会更在下面。

    后来的话: 这篇文章有一些地方讲的还是比较不清晰的,现在已经修正了一部分,并且增添了对一部分知识点的代码,日后也会慢慢修改。如果你发现本篇文章的错误或者对本篇文章有什么建议可以评论区留言或者私信笔者。

                                                                                                                            ——2022.6.1

    展开全文
  • freemarker-pdf-编辑器 一个简单的 web 应用程序,用于编辑 FreeMarker FTL 模板,快速转换并预览 HTML 和 PDF 格式... HTML 预览的工作原理是简单地将整个文档复制到<iframe>的srcdoc属性中,该属性似乎仅适用于
  • 整个项目采用 Electron + Vue + Vuetify 开发,目的是想做一个可视化设计小程序的设计器,导出为 Taro 项目代码。 目前功能: 支持审查/复制单个 Taro UI 组件,修改参数实时可看。 支持查看/复制当前页面代码。 ...
  • 整个正弦结构光模板通过周期性复制编码单元来实现,再通过高分辨率光刻技术加工制作在镀铬玻璃基板表面上。模板调制照明光源后,表征二元编码特征的高频信息直接被投影光学系统滤波,而表征正弦条纹信息的低频信息得以...
  • Fork 或下载整个大学模板存储库。 在rise vision 存储或您的网络托管服务中托管整个“大学模板”文件夹目录。 将链接复制到您托管它的 university-template.html 文件,并将 URL 插入到日程表中。 (您也可以将...
  • 如果你不想使用整个 underscore.js 库但需要一个小巧、优雅的模板引擎,请考虑这个项目。 如何使用 下载库 包括图书馆 < script type =" application/javascript " src =" /path/to/underscore-template.min.js...
  • Xamarin项目模板 此回购包含简化Xamarin开发的项目模板。 我们已经在许多项目中完善了这些模式,并找到了对我们来说运作... 要安装这些文件,只需将.zip文件复制到ProjectTemaplates文件夹中,当创建新的Project / So
  • FFDC 这是一个模板(不是 leiningen),结合了几个开始使用的项目。 包括: 与朋友的朋友 datomic pro(应该很容易被 datomic 免费版替换) 栗子模板与所有不错的重新加载的...整个项目使用 de.sveri.structconver
  • 分叉或下载整个内容模板存储库。 修改restaurant-promotion文件夹目录中的文件以适合您的需求。 (有关如何修改特定元素的信息,请参见下文) 将整个“ restaurant-promotion”文件夹目录托管在rise vision存储...
  • 一个主要示例展示了一个简单的编译器,它将其整个解析树作为单个深度结构化的数据对象传递给模板引擎以进行代码生成。 您所要做的就是将包装器脚本复制到 Octave 搜索路径中的某个位置并调整 Java 类路径,以便找到...
  • 不要复制整个模板 直接编辑此目录中的文件 contrib / tools /-第三方工具 在这里,您可以放任何类型的工具。 请使用此目录,而不要在互联网上随意放置。 它使事情变得更容易寻找别人。 contrib / samples /-配置的...
  • Eclipse 模板使用

    千次阅读 2018-10-02 15:43:33
    java editor template Eclipse中的快速Java代码模板使用 &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 相信大家都用过Eclipse的缩写代码提示功能,如...
  • 橙色报告模板经营分析 BreadPPT 1 LOGO 录入文字内容标题 如要增加标题 整个复制并修改数字 1 2 3 目录 Contents 2 录入文字内容标题 4 LOGO 3 章节过渡页更加清晰 Part1 LOGO 二级目录内容 二级目录内容 二级目录...
  • Recycle 是一种用于管理常用模板和样板的美化复制粘贴实用程序。 创建一次模板后,您可以将其保存为 recycle 以备将来使用。 Recycle 可以管理单个文件甚至整个目录结构的模板。 安装 Recycle 可以通过 pip 安装: ...
  • 原标题:来,今天学习秀米的“复制粘贴”快捷键~说到秀米的复制粘贴那作为秀米真爱粉的你肯定第一反应就是先选中组件/模板然后用鼠标点工具条上面的复制将组件/模板复制到剪贴板再点剪贴板粘贴到编辑区域中 这个方法...
  • 3、将模版中的HTML代码整个复制到对应的jsp中 4、将basepath在移动到head标签中 前端代码插件的添加 因为my eclipse不是专业的前端编辑工具,如HBuilder、WebStorm等,所以想实现辅助输入功能,需...
  • POI操作word模板并生成新的word

    千次阅读 2020-12-23 01:07:02
    如果遇到poi读取例如{name}不能识别为一个整体,可以使用word的域操作,如果不太清楚域的使用,可以这么操作,先在text文档中写好,例如{name},然后再整个复制到word中,不要一个一个在word中敲,不然有可能不会被...
  • 黑马程序员C++笔记---模板

    千次阅读 2022-01-28 14:32:23
    1.1模板的概念 1.2 函数模板 c++另一种编程思想是 泛型编程,利用的技术就是 模板 c++提供两种模板机制:函数模板,类模板 1.2.1 函数模板语法 作用:建立一个通用函数,其函数返回值 和 形参类型可以不具体指定...
  • 繁凡的ACM模板(满注释模板

    万次阅读 多人点赞 2020-04-13 22:00:52
    ACM模板,不同于其他大佬的是我这个模板是满注释的!哪怕是小白的你考场上都能快速上手算法,每一句都有注释帮你快速理解,方便对模板根据题意的修改
  • 我需要将模板的全部内容复制到新文档中。问题是不能复制表。目前,我的代码负责复制像粗体和斜体这样的样式。def get_para_data(output_doc_name, paragraph):output_para = output_doc_name.add_paragraph()for run ...
  • 大多数C ++程序员由于其困惑的性质而远离C ++模板。 反对模板的借口: 很难学习和适应。 编译器错误是模糊的,而且很长。 不值得的努力。 承认模板很难学习,理解和适应。 然而,我们从使用模板中获得的好处将...
  • OpenCV 第七章 模板匹配和图像分割

    千次阅读 2022-01-16 14:36:43
    定义:让模板图像在输入图像中滑动逐像素遍历整个图像进行比较,查找出与模板图像最匹配的部分。 单目标匹配 定义:输入图像中只存在一个可能匹配结果 基本格式如下: result = cv2.matchTemplate(image,templ,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 99,333
精华内容 39,733
关键字:

怎么复制整个模板