• 聊聊 C# 和 C++ 中的 泛型模板 底层玩法


    最近在看 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++ 层面即可

    图片名称
  • 相关阅读:
    论文阅读 dyngraph2vec: Capturing Network Dynamics using Dynamic Graph Representation Learning
    升级openssh的补救
    二阶魔方
    Extra argument start service sshd does not support chkconfig
    通用帮助类集合Shiny.Helper库的使用
    .net core Redis客户端Shiny.Redis包库的使用
    .net core mqtt客户端Shiny.Mqtt库的使用
    基于Sqlsugar单例模式封装的库ShinySqlSugar的使用
    加速训练之并行化 tf.data.Dataset 生成器
    ffmpeg protocol concat 进行ts流合并视频的时间戳计算及其音画同步方式一点浅析
  • 原文地址:https://www.cnblogs.com/huangxincheng/p/16384668.html
Copyright © 2020-2023  润新知