背景
最近在写代码的时候经常被类型推导搞晕,今天好好总结一下
注意
所谓的类型推导只是针对于函数模板,对于类模板而言没有类型推导一说,所以类每次需要编程人员自己指定,但是函数模板可以自动推导,表面上降低了工作量,实际上遇到复杂的状况,直接脑袋宕机不知道编译器推导了个什么玩意。
引言
虽然是讲函数模板推导,但是借用类模板来导出今天的话题, 我这里写了一个类模板,可以看到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);
}
- 先看第一个模板,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也是返回右值引用,而且这种使用场景更多。
- 再看看第二个模板,函数参数其实是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;
}