C++之std::type_identity

news2024/10/6 18:27:28

目录

1.简介

2.C++20的std::type_identity

3.使用 type_identity

3.1.阻止参数推导

3.1.1.模板参数推导过程中的隐式类型转换

3.1.2.强制显式实例化

3.2.阻止推断指引

3.3.类型保持

3.4.满足一些稀奇古怪的语法

4.示例

5.总结


1.简介

   std::type_identity 是 C++17 引入的一个实用工具,用于确保类型别名保持其引用的完整性。在某些模板元编程的场景中,尤其是在与类型萃取(type traits)和完美转发(perfect forwarding)相关的场景中,保持类型的“原样”传递是非常重要的。

   std::type_identity 是一个简单的模板,它定义了一个别名 type,该别名简单地重新声明了其模板参数类型。但重要的是,它不会修改或“破坏”传递给它的类型。

        在 C++17 之前,要实现这样的效果通常需要一些技巧,比如使用结构体的模板特化或复杂的继承层次结构。但是,std::type_identity 使得这个过程变得更加简单和直观。

        我们用一个例子来说明这个问题,尽管这个例子有点微不足道:

template <class T>
T Add(T a, T b) {
    return a + b;
}

Add(4.2, 1); //错误

        尽管将整数与浮点数累加并返回浮点数是 Add 函数应该支持的运算,但是编译器却在这里报错,因为在参数推导的时候,它根据第一个参数推导出 T 是 double 类型,但是根据第二个参数推导出 T 是 int 类型,这产生了矛盾,于是编译器罢工了。

        解决这个问题的方法很简单,比如我们可以谢绝自动推导,用显式实例化(Explicit instantiation)的方式,只是每次调用的时候会麻烦一点:

foo<double>(4.2, 1);

        当然,更常用的实践是借助 C++ 的非推导语境(non-deduced contexts)规避不希望的参数推导,比如下面这种 identity 惯用法:

template< class T >
struct identity {
    using type = T;
};

template <class T>
T Add(T a, typename identity<T>::type b) {
    return a + b;
}

//相当于 double Add(double a, double b)
foo(4.2, 1); //OK, T 被推导为 double

        根据 identity 的定义,identity<T>::type 其实就是 T,为什么加上这个多此一举的东西就 OK 了?这并不是什么黑魔法,它只是借助了模板参数推导规则中最常用的一种非推导语境,即:

对于模板参数中出现的嵌套类型表达式,域解析运算符(::)左边的嵌套名称说明符如果是个限定性说明符(Qualified identifiers),则该嵌套名称说明符不参与模板参数推导。

        所以用了 identity 大法之后,"::type" 左边的 identity<T> 就不参与模板参数推导,T 就是根据第一个参数推导出的 double,identity<T>::type 就也是 double 了。

2.C++20的std::type_identity

        C++ 20 的 type traits 增加了一个 type_identity,其作用和上一节中自定义的 identity<T> 一样,只是不用重复发明轮子了。直接使用 type_identity 的代码是这个样子的:

template <class T>
T Add(T a, typename std::type_identity<T>::type b) {
    return a + b;
}

        C++ 还提供了一个别名:

template< class T >
using type_identity_t = typename type_identity<T>::type;

        使用 type_identity_t<T> 的代码更简单一点:

template <class T>
T Add(T a, std::type_identity_t<T>  b) {
    return a + b;
}

3.使用 type_identity

3.1.阻止参数推导

3.1.1.模板参数推导过程中的隐式类型转换

        其实建立非推导语境的常用目的是让一个模板参数的类型依赖另一个模板参数的推导结果,这种非推导语境的建立还会带来一些意想不到的效果。比如这个例子:

template <typename ...args_t>
void func(std::function<void(args_t...)> function_, args_t ...args){
    /// do something here
}

void func2(std::function<void(int)> function_,  int args) {
    /// do something here
}

void test() {
    func([](int a){ }, 1); //编译错误
    func2([](int a){ }, 1); //没有问题
}

        代码中的 func 和 func2 函数的作用相当于一个调用器(Invoker),区别就是 func 是个函数模板,而 func2 是个普通函数。test 函数中调用 func 会导致编译错误,调用 func2 确实正常的。func2 能正常调用说明从 lambda 到 std::function 的隐式转换是没有问题的,那为什么 func 就不能转换呢?

        原因就是在参数推导的时候是不考虑隐式类型转换的,func 是函数模板,它的第一个参数类型 std::function<> 依赖于对模板参数 args_t 的推导结果,推导出来的 std::function<void(args_t...)> 无法与实参传入的 lambda 表达式类型进行匹配,导致推导最终失败,实际上并没有产生一个类似 :

void func(std::function<void(int)> function_, int)

的推导结果,在随后的重载决议时虽然允许隐式转换,但是因为没有一个上述结果能与之进行转换,最终的结果就是编译失败,错误原因是没有一个 func 的实例能匹配 lambda 表达式的调用。func2 能调用成功是因为 func2 是普通函数,此处会进行隐式类型转换。

        此时如果让 std::function<void(args_t...)> 不参与推导,那么它就不需要与实参传入的 lambda 表达式进行匹配,也就是不会导致推导错误,就能得到上面的推导结果,于是在随后的重载决议的时候就能通过隐式类型转换完成函数调用。此时就需要用 type_identity 建立非推导语境了,我们将 func 的设计改成这样就可以了:

template <typename... args_t>
void func(std::type_identity_t<std::function<void(args_t...)>> function_, args_t ...args) {
    /// do something here
}

3.1.2.强制显式实例化

        利用 type_identity,还可以在设计上要求用户在使用函数模板的时候必须显式指定模板参数,比如这个例子:

template <typename U, typename V>
void foo(std::type_identity_t<U> u, V v) {  ... }

foo<double>(5.9, 6); //编译正确
foo<int>(5.9, 6); // 编译正确,此处发生隐式类型转换
foo(5.3, 6.2); //错误

        foo 函数从设计上强制用户必须指定第一个参数的类型,目的可能是想允许第一个参数的隐式类型转换,也可能是其他目的,总之,使用 type_identity 可以达到这种效果,使用 foo 函数的用户必须显式指定第一个参数的类型。

3.2.阻止推断指引

        C++ 17 引入了一个新的语言特性,就是 CTAD,借助于隐式或显式的推断指引,类模板的模板参数也支持自动推导。但是,隐式的类型推导有可能会产生错误的结果,比如这个 smart_pointer 类的设计:

template <class T>
class smart_pointer {
public:
    smart_pointer(T* object);
    //...
}

        借助于隐式推断指引,用户可以写出这样的代码,不需要显式指定模板参数 T:

Widget* widget{/* ... */};
smart_pointer ptr{widget};

        但是问题是,T* 代表的指针无法区别 object 是单个对象的指针还是数组,因为数组在函数调用的时候也会退化成指针,所以自动推导出来的类型有可能是错误的,比如这样的代码:

Widget widget[N];
smart_pointer ptr{widget};

        此时推导类型 T 仍然是 Widget,我们希望的是 Widget[]。

        借助于 type_identity,我们可以阻止这种隐式的推断指引,强制用户指定正确的模板参数类型。我们将 smart_pointer 的构造函数修改一下:

smart_pointer(std::type_identity_t<T>* object);

        这样上述代码就会产生错误,用户必须这样使用才能正确编译,这也是我们期望的正确结果:

Widget widget[N];
smart_pointer<Widget[]> ptr{widget};

3.3.类型保持

        type_identity_t<T> 本质上还是 T,所以可以被用在一些需要短暂记忆并保持类型的场合。资料 [5] 是 Timur Doumler 为推动 type_identity 进入标准而做的提案,它给出了几个利用 type_identity 的类型保持功能,使得 type_identity 可以作为其他元函数(Meta function)的实现基础,比如我们可以模仿标准库实现一个 remove_const 的元函数(type traits):

template <typename T>
struct remove_const : std::type_identity<T> {};

template <typename T>
struct remove_const<T const> : std::type_identity<T> {};

大家可能会有疑问,为什么不直接写成这样:

template <typename T>
struct remove_const : T {};

template <typename T>
struct remove_const<T const> : T {};

如果写成第二种形式,则这样的断言会失败:

static_assert(std::is_same_v<remove_const<int const>, int>);

        因为通过 remove_const<T const> 或 remove_const<T> 得到的类型都是 remove_const<T>,不是 T。使用 type_identity,我们就可以借助于它的 type 类型保持得到原始的 T,这样的断言就是成功的:

static_assert(std::is_same_v<remove_const2<int const>::type, int>);

        因为通过 remove_const2<T>::type 可以得到原始类型 T,而不是 remove_const2<T>,那不是我们要的结果。

3.4.满足一些稀奇古怪的语法

        type_identity 的其他用法还包括满足一些稀奇古怪的语法形式,比如语法上我们要创建一个临时数组,但是直接写 'T[]{}' 语法形式上就是错误的,因为编译器不能确定 '[]' 的左边是标识符还是类型。但是用 type_identity 中转一下,编译器就确定知道 '[]' 左边是个类型:

template<typename T>
void Print(T v[]) { ... }

template<typename T>
void Process(T t) {
    Print(std::type_identity_t<T[]>{ 1,2,3 }); //编译器能正确理解
    //PrintInt(T[]{ 1,2,3 }); //语法错误,[] 左边不能确定是类型
}

4.示例

代码如下:

#include <iostream>
#include <type_traits>
#include <functional>
 
template <typename T>
struct type_identity
{
    using type = T;
};
 
template <typename T>
using type_identity_t = typename type_identity<T>::type;
 
 
template <typename... args_t>
void func_wrapped(type_identity_t<std::function<void(args_t...)>> function_,
                  args_t ...args)
{
    std::cout << "typeid(function_).name():                         "
              << typeid(function_).name() << std::endl;
    std::cout << "typeid(std::function<void(args_t...)>).name():    "
              << typeid(std::function < void(args_t...)>).name() << std::endl;
    std::cout << "std::is_same<>::value             "
              << std::is_same< std::function<void(args_t...)>,
              type_identity_t<std::function<void(args_t...)>>
              >::value << std::endl << std::endl;
    // do something here
}
 
void test()
{
    std::cout << __FUNCTION__ << std::endl;
}
 
int main()
{
    std::cout << std::boolalpha;
    func_wrapped([](int a) { }, 1);
    func_wrapped(test);
 
    return 0;
}

输出:

typeid(function_).name():                         St8functionIFviEE
typeid(std::function<void(args_t...)>).name():    St8functionIFviEE
std::is_same<>::value             true
 
typeid(function_).name():                         St8functionIFvvEE
typeid(std::function<void(args_t...)>).name():    St8functionIFvvEE
std::is_same<>::value             true

5.总结

        希望大家能够有所收获,笔者水平有限。成文之处难免有理解谬误之处,欢迎大家多多讨论,指教。

推荐文章阅读

std::type_identity

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

运算放大器共模抑制比(CMRR)

目录 运算放大器共模抑制比(CMRR) 三运放共模抑制比电路 运算放大器共模抑制比(CMRR) 如果信号均等施加至运算放大器的两个输入端&#xff0c;使差分输入电压不受影响&#xff0c;则输出也不应受影响。实际上&#xff0c;共模电压的变化会引起输出变化。运算放大器共模抑制比…

鸿蒙实现金刚区效果

前言&#xff1a; DevEco Studio版本&#xff1a;4.0.0.600 所谓“金刚区"是位于APP功能入口的导航区域&#xff0c;通常以“图标文字”的宫格导航的形式出现。之所以叫“金刚区”&#xff0c;是因为该区域会随着业务目标的改变&#xff0c;展示不同的功能图标&#xff…

SpringMVC—RequestMapping注解

一、RequestMapping注解 RequestMapping注解&#xff1a;是Spring MVC框架中的一个控制器映射注解&#xff0c;用于将请求映射到相应的处理方法上&#xff0c;具体来说&#xff0c;他可以将指定URL的请求绑定到一个特定的方法或类上&#xff0c;从而实现对请求的处理和响应。 …

充电学习—7、BC1.2 PD协议

BC1.2&#xff08;battery charging&#xff09;充电端口识别机制&#xff1a; SDP、CDP、DCP 1、VBUS detect&#xff1a;vbus检测 PD&#xff08;portable device&#xff0c;便携式设备&#xff09;中有个检测VBUS是否有效的电路&#xff0c;电路有个参考值&#xff0c;高…

高级算法复习指南

自用 如果帮到了你就点个关注和赞吧&#xff01; 1.算法知识回顾 求两个不全为0的非负整数m和n的最大公约数 数据结构回顾 数组 连续内存分配、静态大小、类型一致、索引访问、访问时间相同、边界检查 多维数组&#xff1a;数组的数组&#xff0c;可以是连续的&#xff0c…

不重新安装Anaconda找回不见的Anaconda Prompt

找回Anaconda Prompt只需三步 系统&#xff1a;win11 x641.cd Anaconda的安装目录2. Anaconda Prompt又回来了 系统&#xff1a;win11 x64 1.cd Anaconda的安装目录 winR 输入cmd 进入命令行&#xff0c;进入到Anaconda的安装目录 eg&#xff1a;我的Anaconda安装在&#xff…

Ptrade和QMT的区别,怎么获取合适的量化交易软件?

​Ptrade和QMT的适用人群 交易活跃用户 量化爱好者已经专业量化投资者 高净值个人或机构 Ptrade和QMT的区别 回测和交易频率 Ptrade回测和交易只支持分钟级和日线级别的频率&#xff0c;而QMT支持tick级、分钟级、5分钟级、10分钟级、日线、周线、月线等。 使用QMT进行回…

GDB调试相关教程

GDB调试相关教程 相关参考链接 https://wizardforcel.gitbooks.io/100-gdb-tips/content/set-step-mode-on.htmlhttps://wizardforcel.gitbooks.io/100-gdb-tips/content/set-step-mode-on.html 设置程序运行参数 命令set args 10 20 30 40 使用show args显示设置好的运行参…

线性稳压器LDO的基础知识

一、什么是线性稳压器? 线性稳压器的工作原理是&#xff1a;采用一个压控电流源以强制在稳压器输出端上产生一个固定电压。控制电路连续监视&#xff08;检测&#xff09;输出电压&#xff0c;并调节电流源&#xff08;根据负载的需求&#xff09;以把输出电压保持在期望的数值…

(微服务实战)聚合支付系统商户线上聚合收银台接口设计

1 概述 聚合支付收款分为线上和线下业务场景&#xff0c;本文中的商户收银台接口设计主要是指线上业务场景&#xff0c;线下业务场景聚合收款方式后续会进行单独设计和分析。 主流的线上支付渠道有微信支付&#xff0c;支付宝支付&#xff0c;云闪付。这三种支付渠道都有各自…

JupyterLab使用指南(五):JupyterLab的 扩展

1. 什么是JupyterLab的扩展 JupyterLab 扩展&#xff08;Extension&#xff09;是一种插件机制&#xff0c;用于增强 JupyterLab 的功能。通过安装扩展&#xff0c;用户可以添加新的功能、改进现有功能&#xff0c;甚至自定义界面和工作流。扩展可以覆盖各种用途&#xff0c;从…

零售门店收银系统源码

智慧新零售系统是一套线下线上一体化的收银系统。致力于给零售门店提供『多样化线下收银』、『ERP进销存』、『o2o小程序商城』、『精细化会员管理』、『丰富营销插件』等一体化行业解决方案&#xff01; 一、多样化线下收银 1.聚合收款码 ①适用商户&#xff1a;小微门店&am…

QT day4(对话框 事件机制)

1&#xff1a;思维导图 2&#xff1a; #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);ui->setupUi(this);//去除头部this->setWindowFlag(Qt::Frameles…

【docker 如何自定义镜像】

查看容器列表 首先是查看容器&#xff1a;在命令台中键入 docker ps -a 命令&#xff0c;得到如下界面。 从容器创建一个新镜像 接着&#xff0c;dockers commit 容器名 要保存成的镜像名&#xff1a;版本名&#xff08;若没有 &#xff1a;版本名 则直接默认为latest&#x…

openwrt如何安装python

首先配置opkg源。 # 备份初始conf mv /etc/opkg.conf /etc/opkg.conf.bak # 新建配置 vim /etc/opkg.conf # 添加如下内容&#xff1a; dest root / dest ram /tmp lists_dir ext /var/opkg-lists option overlay_root /overlay # notice dest usb /mnt/sdb1/opkg arch all 100…

LeetCode739每日温度

题目描述 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c;下一个更高温度出现在几天后。如果气温在这之后都不会升高&#xff0c;请在该位置用 0 来代替。 解析 每次往栈中…

简易计算器需求报告

1. &#xff08;简易计算器&#xff09; 需求说明书 文件编号&#xff1a;2022[1] [木柚2] 06[3] [木柚4] 01[5] [木柚6] 完成日期&#xff1a;2024年 06月18日 编制&#xff1a; 易正阳 日期&#xff1a;2024年6月18日 审核&#xff1a;张正 日期&#xff1a;2024年6月18…

【免费API推荐】: 解锁创意无限,享受免费开发之旅

幂简网站上免费的 API 分类内汇集了各种各样的免费 API&#xff0c;涵盖了多个领域和功能。无论你是在构建网站、开发应用还是进行数据分析&#xff0c;这个项目都能为你提供丰富的选择。 幂简集成搜集了网络上免费的 API 资源&#xff0c;为广大开发者和创业者提供便捷的访问渠…

浏览器渲染机制:重排(Reflow)与重绘(Repaint)以及Vue优化策略

浏览器渲染机制是一个复杂但有序的过程&#xff0c;其目的是将HTML、CSS和JavaScript代码转化为用户可以看到和交互的视觉界面。重排&#xff08;Reflow&#xff09;与重绘&#xff08;Repaint&#xff09;是浏览器渲染过程中对页面元素进行更新的两个重要步骤&#xff0c;理解…