首页 > 其他分享 >究极鬼畜:泛型auto

究极鬼畜:泛型auto

时间:2023-08-13 12:23:08浏览次数:30  
标签:究极 func 函数 int 鬼畜 Tp 参数 template 泛型

究极鬼畜:泛型auto

零 前言

C++ 作为一门强大的语言,标准库中为我们提高了许多相当实用的模板。

然而有时候你 (其实整个机房就我一个) 又自己有一些代码,想转化为一个封装好的板子。

这时候,你就不得不接触泛型编程了。

那个 auto 是为了押韵乱加的,虽然 auto 确实可以用来泛型……

由于作者水平有限,许多地方并不是很严谨,而仅仅是方便理解地进行解释。

一 为什么要泛型?

对于一份代码,如下面这个函数:

int plus(int x,int y){
    return x+y;
}

显然,它有将两个数加起来的功能。

然而当传入的参数类型为 long long,编译器会隐式地(你,即调用者不需要专门体现出来)把这个参数转化为 int 类型,从而造成溢出。

一个无脑地做法是,直接把参数类型都改为最大类型(比如这里用 long long__int128)。

然而如果传入的是浮点数,那么就又出现问题了:小数部分的精度将会丢失。

固然可以对每种类型都写一遍,如这样:

#define __plus__(_Tp)	\
_Tp plus(_Tp x,_Tp y){	\
	return x+y;			\
}
__plus__(long long)
__plus__(double)
......
#undef __plus__

但是我们有更简单,且更全面的做法:

template<typename _Tp>
_Tp plus(_Tp x,_Tp y){
    return x+y;
}

这样,编译器就会为不同的类型都生成出这样一个函数,前提是有代码调用了这个类型的 plus

相比第一种实现,这样做的好处有:

  • 代码更短,更方便。
  • 不需要用 long long 处理 int(当然第一种实现多写几行也可以)。
  • 不会生成较冗余的代码,即没有使用 float 类型的 plus,就不会生成对应的函数。
  • 可以更方便地特化(对某一种情况特殊处理)。
  • ……

可以看到,泛型编程非常实用且有效,因此在各类库中常常会被用到。

(当然平时也可以用,特别是你一份板子写好了就不想翻上去改的情况下。)

二 使用 template

我们来看一下刚刚这段代码:

template<typename _Tp>
_Tp plus(_Tp x,_Tp y){
    return x+y;
}
signed main(){
    plus(1,2);
    plus(1.0,3.2);
}

相较于普通函数,plus 明显地多出了一个叫 template 的东西,这就是模板元参数。

这份代码的原理翻译一下是:

  • template 发现你这玩意儿是泛型,有个 typename 类型的模板参 _Tp,就把编译器踢起来。
  • 编译器出去转了圈,发现 _Tp 传了 intdouble 两种参数。
  • 编译器回来,把这两种参数的代码给写了一遍,然后把 intdouble 填入对应的位置,替代掉那个占位的 _Tp
  • 函数调用的时候,找到最合适的一个(你不可能整型参数的函数不用,跑去用浮点型参数的函数吧)。

值得注意的是,编译器并不能很好地推断你的 _Tp 到底对应了什么类型,他是有要求的:

  • 不能根据返回值推断,如 _Tp min(int x,int y) 就是非法的(鬼知道 _Tp 返回给谁),这种情况下需要手动指定,如 min<int>(3,4)
  • 不能经过类型转化,如 _Tp min(_Tp x,_Tp y) 不能生成供 min(2,3ll) 调用的函数。
  • ……

并且注意,参数推导时可以得到“这是个指针”,但不能发现“这里要引用”或“这有 const 修饰”,因此在函数参数处要把对应的修饰填上。

当然,和函数参数一样,我们也可以有多个模板参,形如:

template<typename _Tp1,typename _Tp2>
bool less(_Tp1 x,_Tp2 y){
    return x<y?x:y;
}

定义起来就和函数一样方便,其实可以将模板参视作编译期间确定的参数,而 typename 占位符表示这传入的是一个类型而非产量。

没错,template 也可以传入常量,形如这样:

template<typename _Tp,int N,int *pri>
void get_prime(_Tp it){
    static bool vis[N];
    for(int i=2;i<N;++i){
        if(!vis[i]) pri[++pri[0]]=i;
        ......
    }
}

这里对 x 有要求,必须是编译期可以知道的整型常量(包括 boolchar,指针……)。

然后如果是编译器不可推断得到的模板参(这里的整型常量显然就不好推断),就需要我们手动指定,形如func<a,b,c,d,..>(x,y,z,...),也即从左到右逐个指定,不可跳过。

但你还可以给一部分模板参预设参数,如:template<typename _Tp1,typename _Tp2=int>,这样如果编译器没办法推断出 _Tp2,会先把这里当作 int 处理。当然,和函数一样,这部分也是需要放在最后几个参数上的。

值得注意的一点是,形如 template<typename _Tp=int>,尽管你都指定了预设的参数,但你调用时若不能推断,仍需要打上一对 <>,因为这里本质上依然是指定参数。

这些大概就是基本的 template 用法。

三 模板参数包

这是一个更高级的模板参数,它的定义形如这样:

template<typename ...Args>
void func(Args ...args){
}

其中 ... 代表着这是一个参数包,即可接受多个参数。

既然如此,那一个函数中显然不能有多个这种参数,且它应该被放在最后。

这样有什么用呢?我们联想一下 C 的 printf,它可以支持任意多个参数,我们的 template 现在也可以了!(当然,C 是用 define 宏实现的,两者不能混为一谈)

那么我们要对这若干个参数进行展开,可以参照以下实现:

template<typename _Tp>
void func(_Tp x){
    //do somethins
}
template<typename _Tp,typename ...Args>
void func(_Tp x,Args ...args){
    func(x),func(args...);
}

这段代码的原理是:在参数大于一个时调用下面的函数进行递归展开,当参数只有一个时调用上面的函数进行处理,其中 args... 意味展开 args 这个参数包。

不过还有另一种做法,不需要进行递归,用到了 C++ 11 的 initializer_list(这是个很轻巧的小东西,你可以放心使用)。

template<typename _Tp>
void func(_Tp x){
    //do somethins
}
template<typename _Tp,typename ...Args>
void func(_Tp x,Args ...args){
    std::initializer_list<int>{(func(args),0/*这里用逗号表达式塞一个0进去,因为前面要求返回值为int*/).../*错误写法 (func(args...),0)*/};
}

上面说到的错误写法相当于把整个参数包展开传给 func,而会调用到自身,造成死循环。正确写法则是只传入一个参数,然后展开这个过程。

除此之外还有个很鬼畜的东西,我们可以用 sizeof...(args)sizeof...(Args) 来得到参数包的大小而不计算……暂时想不出有什么用。

结合前面两部分,我们就可以写出一份漂亮的快读:

template<typename _Tp>
void read(_Tp &x){
    x=0;bool flag=false;char ch=getchar();
    for(;ch<'0'||ch>'9';ch=getchar()) if(ch=='-') flag=true;
    if(flag) for(;ch>='0'&&ch<='9';ch=getchar()) x=(x<<3)+(x<<1)-(ch&15);
    else for(;ch>='0'&&ch<='9';ch=getchar()) x=(x<<3)+(x<<1)+(ch&15);
}
template<typename ...Args>
void read(Args &...args){
    void(std::initializer_list<int>{(read(args),0)...});
}

但是有个问题,这份快读只支持整数类型,那我想读字符串,或者自定义一个类型,怎么办?

看下一部分。

四 特化

模板的特化很简单,比如对于上面的快读,你只需要这样:

template<>//你如果放在前两个函数上面,甚至这行都能删,但不建议删
void read/*这个可以不要*/<std::string>/*就是这部分*/(std::string &str){
    std::cin>>str;//这里就图方便了
}

不过细究下来,特化还是有很多要求,这里暂时就不展开讲了。

五 仿函数

仿函数的本质是类,但调用方式却类似于函数。

常见的仿函数如 std::less 等等用于比较的仿函数,还有一种比较高级的是 std::function

而当你使用 std::less 的时候,会发现STL容器要求你后面不接 (),但如 std::sort 一类的STL函数,则要求你后面接这个 ()

这就要唠嗑一下仿函数的实现了。

我们看这一段代码:

template<typename _Tp>//类的泛型和函数的基本原理类似,但使用时有些不一样的地方,一般不影响
struct less{
    bool operator()(const _Tp &x,const _Tp &y)const{
        return x<y;
    }
};

这里我们重载了 () 运算符,因此可以有这样的调用:

less cmp;
cmp(3,5);

这样,cmp 就可以类似与函数地进行使用。

而STL容器内部,就是用传入的 _Compare 定义了一个 Compare 成员,然后使用该成员进行比较。

但是STL算法提供的接口,要求你使用提供这么一个成员(因为内部是直接调用函数的形式),所以你要在传参的时候构造这么一个对象,也就是调用构造函数生成一个。

仿函数有什么用呢,比如上面,比较运算符不能作为模板参或者函数参数传入,这时你就只能传一个仿函数来实现。

而且我们还可以再来看个好玩的:

struct modify{
    int L,R,Val;
    void func(int p,int l,int r){
        //线段树区间修改……
    }
    modify(int _L,int _R,int _Val):L{_L},R{_R},Val{_Val}{func(1,1,n)}
};
modify(1,n,-114514)

看起来是不是很无聊?其实也有点用处。

我们知道,传参需要往栈里面放参数,这会导致大量的时间和空间消耗。

一种解决方法是借助引用来传入固定不变的参数,同时避免不必要的拷贝,但引用这玩意儿并不是很靠谱,因为其底层实现依然是指针,而你还需要对指针进行解引用,这样一来反而会更慢……

现在我们写出这样一个类,把固定不变的参数作为其成员变量,就可以避免对这部分的传参,起到优化(卡常)的作用。

上面的实现和仿函数存在一点区别,它可以直接调用。事实上,这样的实现更贴近我们下面要介绍的小东西:lambda表达式。

标签:究极,func,函数,int,鬼畜,Tp,参数,template,泛型
From: https://www.cnblogs.com/LQ636721/p/17626381.html

相关文章

  • hutool包将json串转为泛型类出现转换异常
    hutool包将json串转为泛型类出现转换异常问题描述:在使用hutool包的toBean方法转换json为泛型类之后,去获取泛型类的某个字段出现报错。java.lang.ClassCastException:cn.hutool.json.JSONObjectcannotbecasttoXXXX造成原因:当使用JSONUtil将一串json转为一个泛型类,且指定转换......
  • centos7.9究极法-基础篇
    @目录前言Linux发行版本帮助命令man命令help命令文件目录管理命令ls命令pwd命令cd命令mkdir命令cp命令scp命令rm命令mv命令chmod命令chown命令文本内容管理命令cat命令echo命令less命令tail命令用户和组管理命令useradd命令userdel命令passwd命令usermod命令groupadd命令groupdel......
  • C# 中关于 T 泛型【C# 基础】
    〇、前言C#里面的泛型不仅可以使用泛型函数、泛型接口,也可以使用泛型类、泛型委托等等。在使用泛型的时候,它们会自行检测你传入参数的类型,因此它可以为我们省去大量的时间,不用一个个编写方法的重载。与此同时,使用泛型会提升程序的效率。本文将围绕泛型的各个方面,详细看下泛型到......
  • javaee 泛型的上下边界和通配符的使用
    下边界packagecom.test.generic;importjava.util.Collection;publicclassTestGenericClass{ //泛型方法?extendsE:泛型的限定 publicstatic<E>voidmove(Collection<E>from,Collection<?superE>to) { for(Ee:from) { to.add(e......
  • javaee 创建泛型方法
    packagecom.test.generic;importjava.util.Collection;publicclassTestGenericClass{ //泛型方法?extendsE:泛型的限定 publicstatic<E>voidmove(Collection<E>from,Collection<?superE>to) { for(Ee:from) { to.add(e);......
  • 【JAVA】探索泛型与数据结构:解锁高效编程
    引言在当今信息爆炸的时代,数据结构和算法成为了程序员必备的核心技能。而泛型作为Java语言中的一项强大特性,为数据结构和算法的实现提供了更高效、更安全的方式。本文将深入探讨泛型的概念、使用场景以及结合数据结构的应用,为您打开高效编程之道。第一部分:了解泛型1.1为什么使......
  • Apache Rocket MQ 组件 (泛型应用)
     一、实现 importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importorg.apache.commons.lang3.StringUtils;importorg.apache.rocketmq.client.consumer.DefaultMQPushConsumer;importorg.apache.rocketmq.client.consumer.listener.MessageList......
  • C#泛型知识点
    当使用C#编程时,泛型是一种非常强大和有用的功能。泛型允许您编写可重用的代码,以处理不同的数据类型,同时提供类型安全和更高的性能。本篇博客将介绍C#泛型的基本概念、用法和示例代码,以帮助您更好地理解和使用泛型。C#泛型-强大的类型安全在传统的C#编程中,我们通常会使用Object......
  • ts中的泛型
    在TypeScript中我们会使用泛型来对函数的相关类型进行约束这里的函数同时包含class的构造函数因此一个类的声明部分也可以使用泛型那么什么是泛型?如果通俗的理解泛型呐?什么是泛型泛型(Generics)是指在定义函数接口或类的时候不预先指定具体的类型而在使用......
  • 每个.NET开发都应掌握C#泛型的知识点
    C#泛型编程是.NETframework2.0的新特征,它提高了代码安全性和代码重用的问题。由于泛型必须指定类型,正确的使用可以避免了装箱拆箱的问题;使用了泛型后,可以将算法或组件泛型化,基于类型参数支持任意数据类型,从而提高了代码的重用性。C#泛型编程在C#是很重要的特性,建议.NET开发熟......