模板类型推导

背景

最近在写代码的时候经常被类型推导搞晕,今天好好总结一下

注意

所谓的类型推导只是针对于函数模板,对于类模板而言没有类型推导一说,所以类每次需要编程人员自己指定,但是函数模板可以自动推导,表面上降低了工作量,实际上遇到复杂的状况,直接脑袋宕机不知道编译器推导了个什么玩意。

引言

虽然是讲函数模板推导,但是借用类模板来导出今天的话题, 我这里写了一个类模板,可以看到T可以被偏特化为[T, T*, T&, T&&], 此外还有[const T, const T*, const T&, const T&&], 下面的代码可以改吧改吧验证这个想法,举这个例子是说,既然typename T可以偏特化这8种类型,那么是不是也可以推导出这么多类型???

template<typename T>
struct name
{
        static const int a = 1;
};

template<typename T>
struct name<T*>
{
        static const int a = 2;
};

template<typename T>
struct name<T&>
{
        static const int a = 3;
};

template<typename T>
struct name<T&&>
{
        static const int a = 4;
};

int main()
{
        name<const int&&> t;
        std::cout<<t.a<<std::endl;
        return 0;
}

函数类型主要有两种方式,分别是传值和传引用:

传值

template<typename T>
void func( T a)
{
  std::cout<<"func"<<std::endl;
};

int main()
{
  		const int a = 1;
		func(a); //template<> void func<int>(int a)
    	const int& d = a;
  		func(d); //template<> void func<int>(int a)
  
  		int b = 2;
  		func(b); //template<> void func<int>(int a)
  		int& c = b;
  		func(c); //template<> void func<int>(int a)
  
  
  		int* e = &b;
  		func(e);//template<> void func<int *>(int * a)
    	const int* f = &b;
  		func(f);//template<>void func<const int *>(const int * a)

		func(std::move(b)); ///template<> void func<int>(int a)
		
		int g[2]={0,0};
  		func(g);//template<> void func<int *>(int * a)
  		const int g[2]={0,0};
  		func(g);//template<> void func<const int*>(int * a)
  		
		func("hello"); //template<> void func<const char *>(const char * a)
		
 
        return 0;
}

首先,没有报错,讲真这个规则太多了根本记不住,所以只需要记住一个特例就是指针:

  • 当传入类型为指针的时候,会原封不动推导
  • 其他的全部回退为原始类型 raw type= decay<real type>。

所以模板函数参数写成传值最简单。这里注意数组的推导类型,和数组一样, 字面常量的类型本来是const char[N], 但是传值的时候推导会被回退到const char*,这样就会丢失N的信息,因此你也不知道传入的到底是数组还是指针,后面我们还要再讨论这个问题怎么解决。

不管是引用还是万能引用,数组传入后都是int (&a)[N], 但是万能引用的时候T=int (&)[3], 这样enable_if_t<std::is_array_v<T>>就会出错。

template<typename T>
void func(T & a)
{
  std::operator<<(std::cout, "func").operator<<(std::endl);
}
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void func<int[3]>(int (&a)[3])
{
  std::operator<<(std::cout, "func").operator<<(std::endl);
}
#endif
;

template<typename T>
void func(T && a)
{
  std::operator<<(std::cout, "func").operator<<(std::endl);
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void func<int (&)[3]>(int (&a)[3])
{
  std::operator<<(std::cout, "func").operator<<(std::endl);
}
#endif


int main()
{
  int array[3] = {1, 2, 3};
  func(array);
  return 0;
}

此外根据这个特性,我们还可以拿出数组的N的值,以前一直以为template<int N>中的常量值是推导不出来的,其实认知是错误的(这里有个问题就是此数组中的N不能是const int, 必须是一个编译期确定的值)。根据上面的知识我们知道,不管传入引用还是万能引用,最后数组的形参类型都是T(&par)[N], 所以我们如果把形参就写成这样就可以推导出数组的元素个数了,C++真是活到老学到老。

template<typename T, int N>
void function(T (&par)[N])
{
        std::cout<<N<<std::endl;

};
/* First instantiated from: insights.cpp:36 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void function<int, 3>(int (&par)[3])
{
  std::cout.operator<<(3).operator<<(std::endl);
}
#endif
;
int main()
{
        int par[3] = {1,2,3};
        function(par);
        return 0;
}

传引用&

先看代码:

#include <iostream>


template<typename T>
void func( T& a)
{
  std::cout<<"func"<<std::endl;
};

int main()
{
  		const int a = 1;
		//func(a); //template<> void func<const int>(const int& a)
    	const int& d = a;
  		//func(d); //void func<const int>(const int & a)
  
  		int b = 2;
  		//func(b); //template<> void func<int>(int & a)
  		int& c = b;
  		//func(c); //template<> void func<int>(int & a)
  
  
  		int* e = &b;
  		//func(e);//template<> void func<int *>(int *& a)
    	const int* f = &b;
  		//func(f);//template<>void func<const int *>(const int *& a)

		//func(std::move(b)); error
		
		int g[2]={0,0};
  		//func(g);//template<> void func<int[2]>(int (&a)[2])
  		const int m[2]={0,0};
  		//func(m);//template<> void func<const int[2]>(const int (&a)[2])
  		
		//func("hello"); //template<> void func<const char[6]>(const char (&a)[6])
		
 
        return 0;
}

引用推导记起来很简单,就是你原来是什么类型,把其中的&符号去掉就可以了,简单的上面几个例子都满足,比如数组是int[2],那么推导出来后T就是int[2], 字符串常量的默认类型是const char[6], 实际推导出来的T类型就是const char[6]。注意:右值是没办法传入的,会编译报错。

传引用&&

这个也叫万能引用,注意万能引用也是引用,所以根据下面的代码可以知道,普通的左值和引用在函数参数形参类型都是引用,推导T的时候会把&&剔除。但是和普通引用的区别就是,这个玩意支持右值,所以一旦使用传引用,尽量使用万能引用。

template<typename T>
void func( T&& a)
{
  std::cout<<"func"<<std::endl;
};

int main()
{
  		const int a = 1;
		// func(a); //template<> func<const int &>(const int & a)
    	const int& d = a;
  		// func(d); //void func<const int &>(const int & a)
  
  		int b = 2;
  		// func(b); //template<> void func<int &>(int & a)
  		int& c = b;
  		// func(c); //template<> void func<int &>(int & a)
  
  
  		int* e = &b;
  		//func(e);//template<> void func<int *&>(int *& a)
    	const int* f = &b;
  		//func(f);//template<>void func<const int *&>(const int *& a)

		//func(std::move(b)); //template<> void func<int>(int && a)
		//上面很多博客瞎扯淡说T被推导为int&&, 其实并不是,可以用static_assert(std::is_lvalue_reference<T>::value)做实验
		int g[2]={0,0};
  		//func(g);//template<> void func<int(&)[2]>(int (&a)[2])
  		const int m[2]={0,0};
  		//func(m);//template<> void func<const int(&)[2]>(const int (&a)[2])
  		
		func("hello"); //template<> void func<const char(&)[6]>(const char (&a)[6])
		
 
        return 0;
}

扩展

这里主要是对完美转发进行一个记录,很多博客在这里都在瞎扯淡误人子弟。

std::forward

首先看看源码:

template <typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
    return static_cast<T&&>(t);
}

template <typename T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
    static_assert(!std::is_lvalue_reference<T>::value, "Can't forward an rvalue as an lvalue.");
    return static_cast<T&&>(t);
}
  1. 先看第一个模板,std::remove_reference::type会把左值引用类型(int&)和右值引用类型(int&&)中的引用都拿掉,这样函数参数其实就是forward(int& t), 可以看出来该参数就是为左值准备的函数。接着看返回类型,当调用forward<T>(左值)时候有三种情况:
  • T =int&&, 返回右值引用 (int&& && = int &&, 折叠)
  • T=int, 返回右值引用 (int&&, 不用折叠)
  • T=int& , 返回左值引用(int& && = int &, 折叠)
    所以一个左值,我可以根据调控T来随意把他转为左值引用还是右值引用,注意,这里有很多博主说只有传入T=int&&才能保证返回值是右值引用,其实是大错特错,传入T=int也是返回右值引用,而且这种使用场景更多。
  1. 再看看第二个模板,函数参数其实是forward(int&& t),所以一旦forward传入右值就会实例化这个函数,接着看返回类型,当调用forward<T>(右值)时候有三种情况:
  • T =int&&, 返回右值引用 (int&& && = int &&, 折叠)
  • T=int, 返回右值引用 (int&&, 不用折叠)
  • T=int& , 返回左值引用(int& && = int &, 折叠,但是一个该函数做了一个static_assert的限定,不许这种情况发生)
    所以一个右值,只能调控T保证其返回右值引用,T可以是int或者int&&, 记住这个函数不能让右值转为左值引用(本来也不许int& a=1这种写法)。

完美转发

下面是常用的demo, 分析process函数可以知道,不管T推导成什么,arg在函数里都是一个左值,这个一定要理解,所以当调用forward的时候其实会调用上述forward的第一个模板函数。通过执行代码可以知道,当process(move(a))被调用时执行了receive(int&& value)这个函数,也就是说forward转发给了右值引用,根据上面的介绍我们知道,forward返回右值引用的时候T=int或者是int&&都可以,这里要注意,很多博主瞎扯说这时候T会被推导为int&&, 可能这些人觉得左值T被推导为int&, 右值被推导为int&&就理所当然,其实大错特错,这里的T被推导为int,验证方法非常多,但是这里推导为int是100%确定的,希望那些博主能改一下自己文章,真是太误人子弟了。

#include <iostream>
#include <utility>


void receive(int& value) {
    std::cout << "Received lvalue: " << value << std::endl;
}

void receive(int&& value) {
    std::cout << "Received rvalue: " << value << std::endl;
}

template<typename T>
void process(T&& arg) {
    receive(std::forward<T>(arg));
}


int main() {
    int a = 10; 
    process(a);          // 传入左值
    process(std::move(a));  // 传入右值
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值