最近在看 C++ 的方法和类模板,我就在想 C# 中也是有这个概念的,不过叫法不一样,人家叫模板,我们叫泛型,哈哈,有点意思,这一篇我们来聊聊它们底层是怎么玩的?

一:C++ 中的模板玩法

毕竟 C++ 是兼容 C 语言,而 C 是过程式的玩法,所以 C++ 就出现了两种模板类型,分别为:函数模板类模板,下面简单分析一下。

1. 函数模板的玩法

玩之前先看看格式: template <typename T> rettype funcname (parameter list) { }

说实话,我感觉 C++ 这一点就做的非常好,人家在开头就特别强调了,这是一个 template,大家不要搞错了,按照这个格式,我们来一个简单的 Sum 操作,参考代码如下:


#include <iostream> //求和函数
template <typename T> T getsum(T t1, T t2) {
return t1 + t2;
} int main() { int sum1 = getsum<int>(10, 10); long sum2 = getsum<long>(20, 20); printf("output: int:sum=%d, long: sum=%ld", sum1, sum2);
}

接下来我就很好奇,这种玩法和 普通方法 调用有什么不同,要想找到答案,可以用 IDA 去看它的静态汇编代码。

从静态反汇编代码看,当前生成了两个函数符号分别为: j_??$getsum@H@@YAHHH@Zj_??$getsum@J@@YAJJJ@Z,现在我们就搞清楚了,原来一旦给 模板 指定了具体类型,它就生成了一个新的函数符号。

乍一看这句话好像没什么问题,但如果你心比较细的话,会发现一个问题,如果我调用两次 getsum<int> 方法,那会生成两个具体函数吗? 为了寻找答案,我们修改下代码:


int main() { int sum1 = getsum<int>(10, 10); int sum2 = getsum<int>(15, 15);
}

然后再用 IDA 查看一下。

哈哈,可以发现这时候并没有生成一个新的函数符号,其实往细处说:j_??$getsum@H@@YAHHH@Z函数签名组合出来的名字,因为它们签名一致,所以在编译阶段必然就一个了。

2. 类模板的玩法

首先看下类模板的格式:template <typename T1, typename T2, …> class className { };

还是那句话,开头一个 template 暴击,告诉你这是一个模板 , 接下来上一段代码:


#include <iostream> template <typename T> class Calculator
{
public:
T getsum(T a1, T b1) {
return a1 + b1;
}
}; int main() { Calculator<int> cal1;
int sum1 = cal1.getsum(10, 10); Calculator<long> cal2;
int sum2 = cal2.getsum(15, 15); printf("output: sum1=%d, sum2=%ld", sum1,sum2);
}

接下来直接看 IDA 生成的汇编代码。

从上面的方法签名组织上看,有点意思,类名+方法名 柔和到一个函数符号上去了,可以看到符号不一样,说明也是根据模板实例化出的两个方法。

二:C# 中的模板玩法

接下来我们看下 C# 中如何实现 getsum 方法,当我把代码 copy 到 C# 中,我发现不能实现简单的 泛型参数 加减乘除操作,这就太搞了,网上找了下实现方式,当然也可以让 T 约束于 unmanaged,那就变成指针玩法了。


namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Calculator<int> calculator1 = new Calculator<int>();
Calculator<long> calculator2 = new Calculator<long>(); int sum1 = calculator1.getsum(10, 10); long sum2 = calculator2.getsum(15, 15); Console.WriteLine($"sum={sum1}, sum2={sum2}");
Console.ReadLine();
}
} public class Calculator<T> where T : struct, IComparable
{
public T getsum(T a1, T b1)
{
if (typeof(T) == typeof(int))
{
int a = (int)Convert.ChangeType(a1, typeof(int));
int b = (int)Convert.ChangeType(b1, typeof(int)); int c = a + b;
return (T)Convert.ChangeType(c, typeof(T));
}
else if (typeof(T) == typeof(float))
{
float a = (float)Convert.ChangeType(a1, typeof(float));
float b = (float)Convert.ChangeType(b1, typeof(float)); float c = a + b;
return (T)Convert.ChangeType(c, typeof(T));
}
else if (typeof(T) == typeof(double))
{
double a = (double)Convert.ChangeType(a1, typeof(double));
double b = (double)Convert.ChangeType(b1, typeof(double)); double c = a + b;
return (T)Convert.ChangeType(c, typeof(T));
}
else if (typeof(T) == typeof(decimal))
{
decimal a = (decimal)Convert.ChangeType(a1, typeof(decimal));
decimal b = (decimal)Convert.ChangeType(b1, typeof(decimal)); decimal c = a + b;
return (T)Convert.ChangeType(c, typeof(T));
} return default(T);
}
}
}

那怎么去看 Calculator<int>Calculator<long> 到底变成啥了呢? 大家应该知道,C# 和 操作系统 隔了一层 C++,所以研究这种远离操作系统的语言还是有一点难度的,不过既然隔了一层 C++ ,那在 C++ 层面上必然会有所反应。

如果你熟悉 CLR 的类型系统,应该知道 C# 所有的 类 在其上都有一个 MethodTable 类来承载,所以它就是鉴别我们是否生成多个个体的依据,接下来我们用 WinDbg 查看托管堆,看看在其上是如何呈现的。


0:008> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
00007ff9d37638e0 1 24 ConsoleApp1.Calculator`1[[System.Int64, System.Private.CoreLib]]
00007ff9d3763800 1 24 ConsoleApp1.Calculator`1[[System.Int32, System.Private.CoreLib]]

从输出信息看,C++ 层面变成了两个 methodtable 类,如果不信的化,还可以分别查看 mt 下的所有方法。


0:008> !dumpmt -md 00007ff9d37638e0
MethodDesc Table
Entry MethodDesc JIT Name
...
00007FF9D36924E8 00007ff9d37638d0 JIT ConsoleApp1.Calculator`1[[System.Int64, System.Private.CoreLib]]..ctor()
00007FF9D36924E0 00007ff9d37638c0 JIT ConsoleApp1.Calculator`1[[System.Int64, System.Private.CoreLib]].getsum(Int64, Int64) 0:008> !dumpmt -md 00007ff9d3763800
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
00007FF9D36924D0 00007ff9d37637f0 JIT ConsoleApp1.Calculator`1[[System.Int32, System.Private.CoreLib]]..ctor()
00007FF9D36924C8 00007ff9d37637e0 JIT ConsoleApp1.Calculator`1[[System.Int32, System.Private.CoreLib]].getsum(Int32, Int32)

从输出信息看,getsum(Int64, Int64)getsum(Int32, Int32) 方法的入口地址 Entry 是完全不一样的,所以它们是完全独立的个体。

三:总结

当看到 模板泛型 两个词,我感觉前者更 通俗易懂 一些,当给模板赋予不同类型时将会生成新的实例,在 C/C++ 中直接化为不同的函数符号,在 C# 中会生成不同的 MethodTable,由于 C# 远离机器, 所以尽量谈到 C++ 层面即可

聊聊 C# 和 C++ 中的 泛型模板 底层玩法的更多相关文章

  1. 【转载】总结一下Android中主题(Theme)的正确玩法

    http://www.cnblogs.com/zhouyou96/p/5323138.html 总结一下Android中主题(Theme)的正确玩法 在AndroidManifest.xml文件中有& ...

  2. C#语法糖系列 —— 第二篇:聊聊 ref,in 修饰符底层玩法

    自从 C# 7.3 放开 ref 之后,这玩法就太花哨了,也让 C# 这门语言变得越来越多范式,越来越重,这篇我们就来聊聊 ref,本质上来说 ref 的放开就是把 C/C++ 指针的那一套又拿回来了 ...

  3. C#语法糖系列 —— 第三篇:聊聊闭包的底层玩法

    有朋友好奇为什么将 闭包 归于语法糖,这里简单声明下,C# 中的所有闭包最终都会归结于 类 和 方法,为什么这么说,因为 C# 的基因就已经决定了,如果大家了解 CLR 的话应该知道, C#中的类最终 ...

  4. C#语法糖系列 —— 第一篇:聊聊 params 参数底层玩法

    首先说说为什么要写这个系列,大概有两点原因. 这种文章阅读量确实高... 对 IL 和 汇编代码 的学习巩固 所以就决定写一下这个系列,如果大家能从中有所收获,那就更好啦! 一:params 应用层玩 ...

  5. 聊聊 C# 方法重载的底层玩法

    最近在看 C++ 的方法重载,我就在想 C# 中的重载底层是怎么玩的,很多朋友应该知道 C 是不支持重载的,比如下面的代码就会报错. #include <stdio.h> int say( ...

  6. 总结一下Android中主题(Theme)的正确玩法

    在AndroidManifest.xml文件中有<application android:theme="@style/AppTheme">,其中的@style/AppT ...

  7. [译]聊聊C#中的泛型的使用(新手勿入) Seaching TreeVIew WPF 可编辑树Ztree的使用(包括对后台数据库的增删改查) 字段和属性的区别 C# 遍历Dictionary并修改其中的Value 学习笔记——异步 程序员常说的「哈希表」是个什么鬼?

    [译]聊聊C#中的泛型的使用(新手勿入)   写在前面 今天忙里偷闲在浏览外文的时候看到一篇讲C#中泛型的使用的文章,因此加上本人的理解以及四级没过的英语水平斗胆给大伙进行了翻译,当然在翻译的过程中发 ...

  8. [译]聊聊C#中的泛型的使用(新手勿入)

    写在前面 今天忙里偷闲在浏览外文的时候看到一篇讲C#中泛型的使用的文章,因此加上本人的理解以及四级没过的英语水平斗胆给大伙进行了翻译,当然在翻译的过程中发现了一些问题,因此也进行了纠正,当然,原文的地 ...

  9. 转载:C#中的泛型

    泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.NET框架引入了类型参数(type parameters)的概念.类型参数使得设计类和方法时,不必确定一个或多个具 ...

随机推荐

  1. Spring-级联赋值

    一.级联赋值第一种方法 1.创建Emp类 package com.bean; public class Emp { private String EName; private String gende ...

  2. Ubu18下cpptest安装

    1.环境安装 sudo apt install g++ sudo apt install automake sudo apt install autoconf sudo apt install lib ...

  3. TCP/IP 协议标准简单描述

    TCP/IP 协议标准简单描述 说明 分为三部分:中文名称.缩写.说明. 应用层 DNS 域名服务 (DNS) 功能: 将域名转化为IP地址 BOOTP 引导程序协议 (BOOTP) 功能: 允许无盘 ...

  4. JetBrains Rider C# 学习②

    前言 C#从入门到精通 链接:https://pan.baidu.com/s/1UveJI_f-c5Dul3GLIICRHg 提取码:1314 C#入门课程 刘铁猛 链接:https://pan.ba ...

  5. css 实现流光字体效果

    <template> <div>     <p data-text="Lorem ipsum dolor"> Lorem ipsum dolor ...

  6. Java 从零开始实现一个画图板、以及图像处理功能,代码可复现

    Java 从零开始实现一个画图板.以及图像处理功能,代码可复现 这是一个学习分享博客,带你从零开始实现一个画图板.图像处理的小项目,为了降低阅读难度,本博客将画图板的一步步迭代优化过程展示给读者,篇幅 ...

  7. bootStrap简要和学习笔记

    bootStrap简要和学习笔记前端在学些了html.css.JavaScript三件套后,我们感觉前端变数太多了,需要创造力来设计一些可能经常使用的界面啊.按钮样式啊等,也就有了一些前端的框架,那何 ...

  8. ArcGIS使用技巧(六)——数据视图

    新手,若有错误还请指正! 有的时候出图时有很多图层,且范围很大,而出图的范围是大范围的一个部分,当然,可以对各个图层进行裁剪,但是比较麻烦,这里介绍一个比较简单的小技巧. 类似于图1,出图的时候只想显 ...

  9. .NET 程序读取当前目录避坑指南

    前些天有 AgileConfig 的用户反映,如果把 AgileConfig 部署成 Windows 服务程序会启动失败.我看了一下日志,发现根目录被定位到了 C:\Windows\System32 ...

  10. 跟我读CVPR 2022论文:基于场景文字知识挖掘的细粒度图像识别算法

    摘要:本文通过场景文字从人类知识库(Wikipedia)中挖掘其背后丰富的上下文语义信息,并结合视觉信息来共同推理图像内容. 本文分享自华为云社区<[CVPR 2022] 基于场景文字知识挖掘的 ...