C++ 的 Tag Dispatching(标签派发) 惯用法

目录

1.概述

2.标准库中的例子

3.使用自己的 Tag Dispatching

3.1.使用 type traits 技术

3.2.使用 Type_2_Type 技术

4.Tag Dispatching的使用场景

5.总结


1.概述

        一般重载函数的设计是根据不同的参数决定具体做什么事情,编译器会根据参数匹配的原则确定正确的重载版本。但是对于函数模板,其参数类型是泛化的模板参数,此时又如何让编译器选择我们希望的那个函数模板的实例呢?提供特化版本是一个方法,但是如果需要特殊处理的类型很多,就需要搞一大堆特化版本,非常不方便。C++ 11 的语言库提供了 std::enable_if,配合编译器的 SFINAE 原则也可以实现在编译期间的特定选择。C++ 17 还提供了一个 std::void_t,以模板别名定义的语法形式提供了另一种利用 SFINAE 的方法。当然,同样是 C++ 17 提供的 if constexpr 语言特性配合各种 type traits,可以更优雅地实现编译期间的特定选择。但是这一篇我们要介绍的是另一种常用的习惯用法(或技术):Tag Dispatching

        在C++中,标签分发(Tag Dispatching)或标签分派是一种技术,它允许你根据传递给函数的参数类型或某个特定标签来选择不同的函数或函数模板进行执行。这通常用于实现重载函数的泛型版本,其中你可能需要根据参数的某些特性(如类型、状态等)来执行不同的逻辑。

        Tag Dispatching 是一种利用某种类型特征,在一系列重载函数之间进行编译期调度(分派、选择)的技术。Tag Dispatching 并不是 C++ 的某种特性,但是作为一种习惯用法在 C++ 中被广泛应用,尤其是在标准库中。这里说的 tag,其实就是定义一种没有操作、没有数据的类型,将这种类型作为重载函数的一个参数,通过不同的 tag 参数控制编译器的选择。定义一个 tag 非常简单,一般用 struct:

struct tag1 {};
struct tag2 {};

        虽然结构体都是空的,但是在 C++ 编译器看来,tag1 和 tag2 是两个完全不同的类型。基于 Tag Dispatching 的实现就是定义不同的 tag,并将 tag 设计成函数的一个参数。一般会将 tag 设计成 函数的最后一个参数,因为编译器在代码生成的时候对这种完全是空的参数类型会有针对性的优化。具体来说,就是将重载函数设计成这个样子:

template <typename T>
int Function(T t, tag1) { ... }template <typename T>
int Function(T t, tag2) { ... }

这就是所谓的 Tag Dispatching,其实就是利用 tag1 和 tag2 是不同类型的特性,控制编译器在编译期间选择希望的重载版本,实现在编译期间的重载分派,比如:

int a = Function(42, tag1());

可以确保编译器使用第一个模板函数。这只是一个简单的例子,要让编译器能够根据类型自动选择,还需要自定义 type traits,请继续看下去。

2.标准库中的例子

        标准库中大量使用 Tag Dispatching,这一节就介绍一下标准库的 std::advance() 函数。void std::advance(Iter& it, Distance n) 函数的作用是将迭代器向前(或向后)移动 n 个位置。这里需要注意的是,根据迭代器类型的不同,std::advance() 函数内部是不同的实现。比如对于随机类型的迭代器,可以采用高效的 it + n 的形式移动位置,对于不支持随机访问的单向迭代器,只能通过执行 n 次 ++it 的方式移动迭代器,而对于双向类型的迭代器,n 可以是负数,表示向后移动迭代器。

        std::advance() 函数首先针对不同类型的迭代器定义了相应的重载形式:

template <class RAIter, class Distance>
void advance(RAIter& it, Distance n, std::random_access_iterator_tag) {it += n;
}template <class BidirIter, class Distance>
void advance(BidirIter& it, Distance n, std::bidirectional_iterator_tag) {if (n > 0) {while (n--) ++it;}else {while (n++) --it;}
}template <class InputIter, class Distance>
void advance(InputIter& it, Distance n, std::input_iterator_tag) {while (n--) {++it;}
}

这几个重载函数的第三个参数就是所谓的 tag,以 std::input_iterator_tag 为例,标准库中的定义大概是这个样子:

struct input_iterator_tag {};

标准库还定义了 `iterator_traits<>` 类模板用于提取迭代器的 tag,对于支持随机访问的迭代器,它的 iterator_category 被特化处理为:

template <class Iter>
struct iterator_traits<Iter> {....using iterator_category = random_access_iterator_tag;
};

可用 iterator_traits<Iter>::iterator_category 提取 Iter 类型迭代器的分类 tag。最终 advance() 的实现大致是这个样子:

template <class Iter, class Distance>
void advance(Iter& it, Distance n) {advance(it, n, typename std::iterator_traits<Iter>::iterator_category{} );
}

3.使用自己的 Tag Dispatching

3.1.使用 type traits 技术

        在介绍 std::enable_if 和 if constexpr 两个主题的时候,我们提到了 `ToString()` 还可以使用 Tag Dispatching 实现,但是没有详细说明。其实 Tag Dispatching 并不是个复杂的技术,那个例子使用 type traits 技术实现分配选择,本篇就借这个主题把这个例子完整解释一下。

        首先要定义 tag,这个例子需要两个 tag 用于区分两种情况:

struct NumTag {};
struct StrTag {};

        理论上说,此时用 `ToString(42, NumTag())` 和 `ToString(std::string("Emma"), StrTag())` 就能区分两个重载函数了,但是我们设计的是针对泛型的函数模板,需要提供一种根据类型提取 tag 的手段。其实就是仿照标准库的样子做一个自己的 traits 类,利用 traits 类的特化版本实现编译期间的 tag 定义:

template <typename T>
struct traits
{typedef NumTag tag;
};template <>
struct traits<std::string>
{typedef StrTag tag;
};

        可以使用 `traits<T>::tag` 提取 T 对应的 tag,针对 `std::string` 提供了一个 `traits<>` 的特化版本,这个版本里的 tag 被定义为 `StrTag`。

        接下来就是实现针对两种 tag 的 `ToString()` 重载版本,为了区分,我们使用 `ToString_impl()` 作为函数名字:

template <typename T>
auto ToString_impl(T t, NumTag)
{return std::to_string(t);
}template <typename T>
auto ToString_impl(T t, StrTag)
{return t;
}

        对于数字类型的数据,用 `std::to_string()` 转换,对于字符串类型的数据,直接返回字符串即可。`ToString_impl()` 函数的第二个参数是哑形参,不需要指定参数名称,编译器会针对这种情况做适当的优化(优化掉这个参数),如果指定参数名字反而会影响编译器的优化判断。

        最后就是提供统一的 `ToString()` 函数,通过 `traits<T>` 提取类型的对应的 tag,让编译器根据 tag 选择正确的重载函数:

template <typename T>
auto ToString(T t)
{return ToString_impl(t, typename traits<T>::tag());
}int main()
{std::cout << ToString(42) << std::endl;std::cout << ToString(std::string("Emma")) << std::endl;
}

3.2.使用 Type_2_Type 技术

        `Type_2_Type` 是一种类型映射技术,常用来将一种普通类型映射为另一种可控类型。Tag Dispatching 也可以借助 `Type_2_Type` 实现类型分派,此时的 tag 也被称为 templated tags。

        首先需要定义一个泛化的 `TypeTag<T>`,用作控制分派的可控类型:

template<typename T>
struct TypeTag {};

        然后修改 `ToString_impl()` 的参数类型,改用我们定义的可控类型做模板参数:

template <typename T>
auto ToString_impl(T t, TypeTag<int>)
{return std::to_string(t);
}template <typename T>
auto ToString_impl(T t, TypeTag<std::string>)
{return t;
}

        最后就是修改 `ToString()` 函数,根据函数参数 t 推导出的类型 T,利用 `TypeTag<T>` 映射为可控类型中的 `TypeTag<int>` 或 `TypeTag<std::string>`,使得编译器可以根据 `TypeTag<T>` 选择正确的重载函数:

template <typename T>
auto ToString(T t)
{return ToString_impl(t, TypeTag<T>());
}

4.Tag Dispatching的使用场景

        编译期需要进行的重载函数分派可以考虑用 Tag Dispatching,运行期间的分派可以考虑 C++ 对象的抽象和分派方式。什么情况适合放在编译期分派呢?对操作或行为需要进行额外控制的场合可以考使用这种编译期进行的 Tag Dispatching,因为这对提高代码运行时的效率非常有用(不需要在运行时对条件进行判断) 。对数据的额外处理就不适合在编译期间决定,因为数据是运行期变化的。

        以下是Tag Dispatching在C++中的一些典型应用场景:

  1. 算法特化(Algorithm Specialization):当算法对于不同的数据类型有不同的最优实现时,可以使用Tag Dispatching来提供特化的版本。例如,对于交换两个元素的操作,对于基本类型可能需要三次拷贝操作,但对于像std::vector这样的容器类型,可以直接使用其成员函数swap来避免拷贝,从而提高效率。
  2. 迭代器类型的优化:在STL(Standard Template Library)中,不同的容器类型具有不同类型的迭代器(如输入迭代器、前向迭代器、双向迭代器和随机访问迭代器)。对于某些算法,根据迭代器的类型选择最优的实现方式可以提高效率。通过使用Tag Dispatching,可以为不同类型的迭代器提供特化的算法实现。
  3. 类型属性的判断:当需要根据类型的某些属性(如是否为整数类型、是否支持某种操作等)来选择不同的行为时,可以使用Tag Dispatching。通过定义与这些属性相关的标签类型,并在函数模板中使用这些标签作为参数,可以在编译时根据类型属性选择正确的实现。
  4. 编译时条件判断:在某些情况下,可能需要在编译时根据某些条件选择不同的函数实现。通过使用if constexpr和Tag Dispatching,可以在编译时根据条件选择并执行相应的函数模板。
  5. 模板元编程:Tag Dispatching在模板元编程中也有广泛应用。通过定义与类型特征相关的标签类型,并在模板元函数中使用这些标签作为参数,可以在编译时根据类型特征执行不同的元编程逻辑。
  6. 类型安全的接口设计:在设计类型安全的接口时,可以使用Tag Dispatching来确保函数只接受特定类型的参数。通过定义与参数类型相关的标签类型,并在函数模板中使用这些标签作为参数,可以在编译时检查参数类型,从而提高代码的类型安全性。

5.总结

        总结来说,Tag Dispatching在C++中主要用于实现泛型算法的优化、迭代器类型的优化、类型属性的判断、编译时条件判断、模板元编程以及类型安全的接口设计等方面。通过使用Tag Dispatching技术,可以根据参数类型或特性在编译时选择最优的实现路径,从而提高代码的性能和可维护性。

推荐阅读:

标签派发

C++之多层 if-else-if 结构优化(二)

C++17之std::invoke: 使用和原理探究(全)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/339339.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据库之函数、存储过程

函数、存储过程 1.函数 函数&#xff0c;常用于对一个或多个输入参数进行操作&#xff0c;主要目的是返回一个结果值&#xff0c;就是一种方法&#xff0c;在postgre里存放的位置叫function&#xff0c;比如创建一个计算长方面积的函数。 举例&#xff1a;建立一个计算长方形…

基于GTX 8B10B编码的自定义PHY接收模块(高速收发器十三)

点击进入高速收发器系列文章导航界面 前文完成了发送模块的设计&#xff0c;本文接着完成接收模块的设计&#xff0c;接收模块相对发送模块会更加麻烦。 1、设计思路 前文在讲解官方示例工程时&#xff0c;提到GTX IP的接收部分没有做字对齐&#xff0c;需要用户自己编写字对齐…

---初始Linux---

一、认识计算机 计算机 硬件 软件 硬件&#xff1a;就是计算机系统中由电子、机械和光电元件等组成的各种物理装置的总称&#xff08;CPU\GPU\...&#xff09; 软件&#xff1a;是用户和计算机硬件之间及进行交流的工具 然而一个简单的计算机或者说基本的计算机就是有两大…

Android开机动画,framework修改Bootanimation绘制文字。

文章目录 Android开机动画&#xff0c;framework修改Bootanimation动画绘制文字。 Android开机动画&#xff0c;framework修改Bootanimation动画绘制文字。 frameworks/base/cmds/bootanimation/bootanimation.cpp 绘制时间的一个方法 // We render 12 or 24 hour time. void…

Linux 僵尸进程和孤儿进程

一.Z(zombie)-僵尸进程 1.僵死状态&#xff08;Zombies&#xff09;是一个比较特殊的状态。当进程退出并且父进程&#xff08;使用wait()系统调用后&#xff09;没有读取到子进程退出的返回代码时就会产生僵死(尸)进程 2.僵死进程会以终止状态保持在进程表中&#xff0c;并且会…

Spring 中如何控制 Bean 的加载顺序?

如果你脱口而出说添加 Order 注解或者是实现 Ordered 接口&#xff0c;那么恭喜&#xff0c;你掉坑了。 一 Order 注解和 Ordered 接口 在 Spring 框架中&#xff0c;Order 是一个非常实用的元注解&#xff0c;它位于 spring-core 包下&#xff0c;主要用于控制某些特定上下文…

SQL实验 带函数查询和综合查询

一、实验目的 1&#xff0e;掌握Management Studio的使用。 2&#xff0e;掌握带函数查询和综合查询的使用。 二、实验内容及要求 1&#xff0e;统计年龄大于30岁的学生的人数。 --统计年龄大于30岁的学生的人数。SELECT COUNT(*) AS 人数FROM StudentWHERE (datepart(yea…

小公司的软件开发IT工具箱

目录 工具链困境 难题的解决 达到的效果 资源要求低 工具箱一览 1、代码管理工具 2、自动化发版&#xff08;测试&#xff09;工具 3、依赖库&#xff08;制品包&#xff09;管理 4、镜像管理 5、授权管理&#xff08;可选&#xff09; 待讨论&#xff1a;为什么不是…

基于全志T507-H的Linux-RT实时性测试案例分享

本文将为各位工程师演示全志T507-H工业评估板&#xff08;TLT507-EVM&#xff09;基于IgH EtherCAT控制伺服电机方法&#xff0c;生动说明Linux-RT Igh EtherCAT的强大之处&#xff01; Linux-RT系统的优势 内核开源、免费、功能完善。 RT PREEMPT补丁&#xff0c;使Linux内…

【Qt】对话框

文章目录 1 :peach:对话框介绍:peach:2 :peach:对话框的分类:peach:2.1 :apple:模态对话框:apple:2.2 :apple:非模态对话框:apple:2.3 :apple:混合属性对话框:apple: 3 :peach:Qt 内置对话框:peach:3.1 :apple:消息对话框 QMessageBox:apple: 1 &#x1f351;对话框介绍&#x…

AK F.*ing leetcode 流浪计划之费马小定理与组合数取模

欢迎关注更多精彩 关注我&#xff0c;学习常用算法与数据结构&#xff0c;一题多解&#xff0c;降维打击。 费马小定理与证明 参考 https://zhuanlan.zhihu.com/p/594859227 费马小定理&#xff1a;如果p是一个质数&#xff0c;而正整数a不是p的倍数&#xff0c;那么a(p-1)≡…

LabVIEW齿轮调制故障检测系统

LabVIEW齿轮调制故障检测系统 概述 开发了一种基于LabVIEW平台的齿轮调制故障检测系统&#xff0c;实现齿轮在恶劣工作条件下的故障振动信号的实时在线检测。系统利用LabVIEW的强大图形编程能力&#xff0c;结合Hilbert包络解调技术&#xff0c;对齿轮的振动信号进行精确分析…

opensips 3.5的DB部署

opensips 3.X的DB部署方式较之前版本有很大的不同。本文以opensips 3.5 为例&#xff0c;说明部署的过程。 当OpenSIPS安装完成后&#xff0c;需要进一步做什么&#xff1f;最大的可能就是部署配套的DB。因为很多功能离不开它&#xff0c;比如用户鉴权、注册信息持久化、dialog…

MySQL学习——影响选项文件处理的命令行选项和程序选项修改器

大多数支持选项文件的MySQL程序都处理以下选项。因为这些选项会影响选项文件的处理&#xff0c;所以必须在命令行上给出&#xff0c;而不是在选项文件中给出。为了正常工作&#xff0c;这些选项中的每一个都必须先于其他选项给出&#xff0c;但以下情况除外&#xff1a; -prin…

OpenCASCADE开发指南<十四>:OCCT建模类之BRepPrimAPI_MakePipe创建管道

1、OpenCasCade拓扑几何 在Open CASCADE Technology (OCCT) 中,除了基本三维几何体建模类BRepBuilderAPI外,还提供了复杂模型的建模类,常用的有如下几种,他们可以单独使用或相互组合,通过OCCT提供的融合函数进行组装。例如:BRepOffsetAPI_ThruSections、BRepOffsetAPI_Ma…

sqlite基本操作

简介 文章目录 简介1.数据库的安装2.数据库命令&#xff1a;API&#xff0c;创建表单代码 csprintf&#xff08;&#xff09;getchar和scanf&#xff08;&#xff09; 1.数据库的安装 sudo dpkg -i *.deb这个报错表明出现依赖问题 用这个命令后再试试sudo apt --fix-broken in…

Docker是什么?使用场景作用及Docker的安装和启动详解

目录 Docker是什么&#xff1f; Docker的发展 Docker的安装 Docker使用 Docker的运行机制 第一个Docker容器 进入Docker容器 客户机访问容器 Docker是什么&#xff1f; Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言 并遵从 Apache2.0 协议开源。 Docker …

ChatGPT的基本原理是什么?又该如何提高其准确性?

在深入探索如何提升ChatGPT的准确性之前&#xff0c;让我们先来了解一下它的工作原理吧。ChatGPT是一种基于深度学习的自然语言生成模型&#xff0c;它通过预训练和微调两个关键步骤来学习和理解自然语言。 在预训练阶段&#xff0c;ChatGPT会接触到大规模的文本数据集&#x…

绘画参数配置及使用

绘画参数配置及使用 路径&#xff1a;站点后台-功能-AI绘画 进入参数配置 接口选择&#xff1a;多种接口自主选择&#xff08;需自己准备key&#xff09;&#xff0c;对应接口的key对话和绘画通用 存储空间&#xff1a; 位置在超管后台-存储空间 自主选择存储&#xff08;需…

冯喜运:6.3周一黄金原油行情分析及操作建议

【黄金消息面分析】&#xff1a;上周行情概述&#xff1a;现货黄金上周&#xff08;0527-0531&#xff09;反弹上探&#xff0c;5月27号开盘前本人曾提醒关注反弹&#xff0c;较当时上涨约30美元&#xff0c;最高至2364一线&#xff0c;其后震荡下跌。周线小幅收跌0.27%&#x…