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: 使用和原理探究(全)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

域内攻击--->基于资源的约束委派(RBCD)

不同于约束和非约束委派&#xff0c;基于资源的约束性委派可以就难的多了&#xff01;&#xff01; 前方高能 &#xff0c;准备上车&#xff01;&#xff01; 目录 1.基于资源的约束性委派(RBCD) 2.谁能设置RBCD 3.机器入域账号的普及 4.域树的搭建 5.配置RBCD 6.通过域创…

【前端部署——vercel】部署next.js使用了prisma的项目

部署流程参考 https://blog.csdn.net/qq_51116518/article/details/137042682 问题 PrismaClientInitializationError: Prisma has detected that this project was built on Vercel, which caches dependencies. This leads to an outdated Prisma Client because Prisma’s …

kali系统baopoWiFi密码

kali系统baopoWiFi密码,仅供学习 取决强大的密码字典,如果别人密码设置的足够安全,也无法破解成功,并不是100%破解 一、准备一个无线网卡&#xff0c;需要免驱动&#xff0c;最好知道频率2.4HGZ还是5.0GHZ 二、插上USB接口&#xff0c;vmware模拟器选择连接虚拟机 三、输入命…

Java Spring Boot 从必应爬取图片

获取图片主要就是通过必应图片页面控制台的元素&#xff0c;确认图片和标题在哪个类中&#xff08;浏览器 F12&#xff09; 引入依赖 这里需要引入两个依赖 jsoup 和 hutool maven依赖网站地址&#xff1a;Maven Repository: Search/Browse/Explore (mvnrepository.com) 挑选…

Java如何读取resources目录下的文件路径(九种代码示例教程)

本文摘要&#xff1a;Java如何读取resources目录下的文件路径 &#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主。公粽号&#xff1a;洲与AI。 &#x1f91…

翻译《The Old New Thing》- What a drag: Dragging a Uniform Resource Locator (URL)

What a drag: Dragging a Uniform Resource Locator (URL) - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20080312-00/?p23133 Raymond Chen 2008年03月12日 麻烦的拖拽&#xff1a;拖拽统一资源定位符&#xff08;URL&#xff09; 简要 …

HALCON-从入门到入门-图像格式的互相转换

1.废话 上次说到了图片的读取和写入到本地&#xff0c;这次说一下图片的格式相关。 位图和矢量图 photoshop处理出来的图片肯定叫做图片&#xff0c;那么coreDraw处理出来的图片是不是也叫图片。 之间就有区分&#xff0c;一种叫做位图&#xff0c;一种叫做矢量图 位图和矢…

STM32作业实现(四)光敏传感器

目录 STM32作业设计 STM32作业实现(一)串口通信 STM32作业实现(二)串口控制led STM32作业实现(三)串口控制有源蜂鸣器 STM32作业实现(四)光敏传感器 STM32作业实现(五)温湿度传感器dht11 STM32作业实现(六)闪存保存数据 STM32作业实现(七)OLED显示数据 STM32作业实现(八)触摸按…

曝光超1.5亿,迪丽热巴“抖音直播首秀”解锁德施曼智能锁科技革命

作为中国电商行业年中最大的消费狂欢节点&#xff0c;今年的618大促热闹依旧&#xff1b;各大品牌在今年极简的现货模式下展开了周期最长的品牌实力比拼。其中&#xff0c;高端智能锁领军品牌德施曼在618大促期间&#xff0c;携手代言人迪丽热巴&#xff0c;再次掀起智能锁消费…

【前端】Vuex笔记(超详细!!)

最近花了两周时间&#xff0c;完完全全的跟着Vuex官方的视频学完了Vuex并且详详细细的做了笔记&#xff0c;其中总结部分是我对于整个视频课程的总结&#xff0c;视频部分是跟着视频做的笔记&#xff0c;如果总结部分有不懂的话&#xff0c;直接去视频部分查找对应的笔记即可&a…

Codeforces Round 548 (Div. 2) C. Edgy Trees

Edgy Trees time limit per test: 2 second memory limit per test: 256 megabytes input: standard input output: standard output You are given a tree (a connected undirected graph without cycles) of n n n vertices. Each of the n − 1 n - 1 n−1 edges of the t…

计算机毕业设计 | SpringBoot招投标系统 任务发布网站(附源码)

1&#xff0c;绪论 在市场范围内&#xff0c;任务发布网站很受欢迎&#xff0c;有很多开发者以及其他领域的牛人&#xff0c;更倾向于选择工作时间、工作场景更自由的零工市场寻求零散单子来补贴家用。 如今市场上&#xff0c;任务发布网站鱼龙混杂&#xff0c;用户需要找一个…

【TCP协议中104解析】wireshark抓取流量包工具,群殴协议解析基础

Tcp ,104 ,wireshark工具进行解析 IEC104 是用于监控和诊断工业控制网络的一种标准&#xff0c;而 Wireshark则是一款常用的网络协议分析工具&#xff0c;可以用干解析TEC104 报文。本文将介绍如何使用 Wireshark解析 IEC104报文&#xff0c;以及解析过 程中的注意事项。 一、安…

STL-queue的使用及其模拟实现

在C标准库中&#xff0c;队列(queue)是一种容器适配器&#xff0c;它以先进先出的方式组织数据&#xff0c;其中从容器一端插入元素&#xff0c;另一端取出元素。 queue的使用 queue的构造函数 queue的成员函数 empty&#xff1a;检测队列是否为空size&#xff1a;返回队列中有…

7-14 字节序(Endianness)---PTA实验C++

一、题目描述 “内存寻址的最小单位是字节”——明白。 “每个字节有唯一的编号&#xff0c;称为地址”——明白。 “C中int通常为四个字节”——了解。 “int x 1;最低字节是1还是0&#xff1f;——纳尼&#xff1f; 事实上&#xff0c;这里有点小小分歧&#xff1a; 多字…

C++对C的增强

1、作用域运算符 ::解决归属问题&#xff08;谁是谁的谁&#xff09; 可以优先使用全局变量 2、命名空间 使用关键字namespace&#xff0c;控制标名称的作用域。 命名空间的本质&#xff1a;对符号常量、变量、函数、结构、枚举、类和对象等等进行封装 1、创建一个命名空间…

学习小记录——python函数的定义和调用

今日小好运&#xff0c;未来有好运。&#x1f381;&#x1f496;&#x1fad4; 分享个人学习的小小心意&#xff0c;一起来看看吧 函数的定义 函数通常来说就是带名字的代码块&#xff0c;用于完成具体的工作&#xff0c;需要使用的时候调用即可&#xff0c;这不仅提高代码的…

我的创作纪念日-砥砺前行

机缘 大家好&#xff0c;我是诊断协议那些事儿&#xff0c;又和大家见面了&#xff0c;记录一下创作日记&#xff0c;转眼间已经在CSDN平台创作三年了&#xff0c;最初仅仅是为了记录学习过程中的笔记&#xff0c;后来慢慢转为项目实践中的经验分享&#xff0c;当然更多的希望…

dm8 什么时候视图中统计的内存会超过OS

v$bufferpool和v$mem_pool视图记录着DMSERVER各组件的内存占用量。理论上跟OS看到的保持一致。但实际大多数场景下&#xff0c;OS中看到的数据远大于视图中的统计。这里面可能有内存泄漏的原因。不过也有的时候视图中的统计数据超过OS。下面就是这种情况&#xff1a; 上图中红线…

nas连接萤石云摄像机CTQ6X

需要准备的nassurveillance 请参考这个大佬的流程 https://www.bilibili.com/video/BV1ri4y1g7EN/ 踩坑&#xff1a; 一直到添加录像机验证一直没问题&#xff0c;但是验证一直不通过&#xff0c;后面下载了萤石云工作室的win桌面客户端&#xff0c;不知道是不是设置了预览还…