C11简洁之道:类型推导
1、 概述
C++11里面引入了auto和decltype关键字来实现类型推导,通过这两个关键字不仅能方便的获取复杂的类型,还能简化书写,提高编码效率。
2、 auto
2.1 auto关键字的新定义
auto关键字并不是一个全新的关键字,早在旧标准中就已经有定义:“具有自动储存期的局部变量”,不过用处并不大,如下:
auto int i = ; //c++98/03,可以默认写成int i = 0; static int j = ;
上述代码中,auto int是旧标准中的用法,与之相对的是static int,代表了静态类型的定义方法,我们很少这样使用auto,因为非static的局部变量本身就具有自动存储期。
所以,c++11中考虑到之前的auto使用较少,不再表示存储类型指示符(如static,mutable等),而是改成了类型指示符,用来提示编译器对此类型的变量做类型的自动推导。
我们来看一组例子:
auto x = ; //OK,x:int
auto pi = new auto(); //OK,pi:int *
const auto *v = &x, u = ; //OK,v:const int *, u:const int
static auto y = 0.0; //OK,y:double
//auto int r; //error,auto不再表示存储类型指示符
//auto s; //error,无法推导出类型
x被自动推导为int,并被初始化为5;
pi被推导为int *,同时说明auto还支持new的类型推导;
&x类型为int *,所以const auto *说明auto是int,所以v为const int *;
因为v为int *,所以推导auto为ing,u也应该为int,这里需要注意的是,u的初始化必须和前面推导的auto类型相同,不然会出现二义性,否则编译器通不过;
y被推导为double;
auto已经不再是存储类型指示符,所以,r会提示错误;
s没类型进行推导,所以会报错没有初始化。
由列子可以得出以下结论:
auto并不能代表一个实际的类型,只是一个类型申明的占位符;
auto申明的变量必须马上初始化,让编译器在编译期间推导出实际类型,在编译的时候用实际的类型替换掉类型占位符auto。
2.2 auto推导规则
auto可以同指针引用结合起来使用,还可以带上cv限定符(const,volatile的统称)。下面我们再来看一组列子:
int x = ;
auto *a = &x; //a -> int*,auto->int
auto b = &x; //b -> int*,auto->int*
auto &c = x; //c -> int&,auto->int
auto d = c; //d -> int ,auto->int const auto e = x; //e->const int
auto f = e; //f->int const auto &g = x; //g->const int &
auto &h = g; //f->const int &
上述例子的推导结果如代码,从例子我们可以得出以下结论:
auto可以自动推导指针类型;
当不申明为指针或者引用时,auto的推导结果和初始化表达式抛弃引用和cv限定符后的类型一致;
当申明为指针或者引用时,auto推导的结果将保持初始化表达式的cv属性。
2.3 auto的限制
auto申明的时候必须初始化,那么auto肯定是不能作为函数参数的。
void func(auto a = 2){} //error:不能用于函数参数
auto不能用于非静态成员变量。
struct Foo
{
auto var1 = ; // error
static const auto var2 = ; //OK,var2->static const int
}
auto无法定义数组。
int arr[] = {};
auto aa = arr; //OK,aa->int *
auto arr2[] = {}; //error无法通过编译
auto无法推导出模版参数。
std::vector<int> vec1; //OK
std::vector<auto> vec2 = vec1; //error 无法通过编译
在Foo中,auto仅能推导static const的整型或者枚举成员(因为其他静态类型在c++标准中无法就地初始化),c++11中可以接受非静态成员变量的就地初始化,但不支持auto类型非静态成员变量的初始化。
2.4 auto使用场景
auto在我看来,最主要的是可以简洁代码。类型推导虽然说可以作自动推导,但是在真正写代码的时候,还要考虑可读性,auto并不能代码更多的好处。
如果在c++11之前,我们定义一个map,在遍历的时候,通常需要这么写:
void func()
{
map<int, int> test_map;
map<int, int>::iterator it = test_map.begin(); for(it; it != test_map.end(); ++it)
{
//do something
}
}
那么我们在使用auto之后,代码就会很简单了,根据map.begin()就可以推导出类型。
void func2()
{
map<int, int> test_map; for(auto it = test_map.begin(); it != test_map.end(); ++it)
{
//do something
}
}
是不是感觉简洁多了。更简洁的还在后面,我们在一个unordered_multimap中查找一个范围:
void func3()
{
unordered_multimap<int, int> test_map;
pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> range = test_map.equal_range();
}
很明显,这个euqal_range返回的类型显得太繁琐,而实际上可能并不在乎这里的具体类型,这时就可以通过auto来简化书写。使用auto之后:
void func4()
{
unordered_multimap<int, int> test_map;
auto range = test_map.equal_range();
}
如果在某些情况不知道返回值类型,我们可以通过auto来做推导,然后统一处理。
class Foo
{
public:
static int get()
{
return ;
}
}; class Bar
{
public:
static const char *get()
{
return "";
}
}; template <class A>
void func5()
{
auto val = A::get();
// do something
} void func6()
{ func5<Foo>();
func5<Bar>();
return;
}
假如我们定义一个泛型函数func5,对具有静态方法get的类型A得到的结果做统一的后续处理,如果不使用auto,那么就必须再增加一个模板参数,并在外部手动指定get的返回值类型。
auto是一个很强大的工具,但是如果不加选择的随意使用auto会导致代码可读性和可维护性严重下降,因此,在使用的时候,一定要权衡好使用的“度”,那么带来的价值会非常大。
3、 decltype
3.1 基本定义
auto的申明必须要初始化才能确定auto代表的类型,那如果我们既需要得到类型,又不定义变量的时候怎么办呢?C++11提供了decltype来解决这个问题,它的定义如下:
decltype(exp) //exp表示一个表达式。
decltype有点像sizeof,不过sizeof是计算表达式类型大小的标识符。和sizeof一样,decltype也是在编译时期推导类型的,并且不会真正计算表达式的值。我们来看一组例子:
int x = ;
decltype(x) y = ; //y->int
decltype(x+y) z = ; //z->int const int & i = x;
decltype(i) j = y; //j->const int &
const decltype(z) *p = &z; //*p->const int, p->const int *
decltype(z) *pi = &z; //*pi->int, pi->int *
decltype(pi) * pp = π // *pp->int *, pp->int **
decltype和auto一样,可以加上引用指针,以及cv限定符进行推导。
3.2 推导规则
网上各种版本的规则众多,下面是我简单整理的一些规则:
exp是标识符、类访问表达式,decltype(exp)和exp的类型一致;
exp是函数调用,decltype(exp)和函数返回值一致;
exp是一个左值,则decltype(exp)是exp的一个左值引用,否则和exp的类型一致。
3.2.1 标识符表达式和类访问表达式
class Foo
{
public:
static const int miNum = ;
int ix;
}; int n = ; volatile const int &x = n;
decltype(n) a = n; //a->int
decltype(x) b = n; //b->const valatile int & decltype(Foo::miNum) c = ; //c->const int Foo foo;
decltype(foo.ix) d = ; //d->int,类访问表达式
变量abc保留了表达式的所有属性(cv、引用),对于表达式,decltype的推导和表达式一致,而d是一个类访问表达式,因此也和表达式类型一致。
3.2.2 函数调用
int &func_int_r(void); //左值,lvalue,简单理解为可寻址值
int &&func_int_rr(void); //x值,xvalue,右值引用本身是一个xvalue
int func_int(void); //纯右值,pvalue const int &func_cint_r(void); //左值
const int && func_cint_rr(void); //x值
const int func_cint(void); //纯右值 cont Foo func_foo(void); //纯右值
//下面是测试语句
int x = ; decltype(func_int_r()) al = x; //al->int&
decltype(func_int_rr()) bl = ; //bl->int &&
decltype(func_int()) cl = ; //cl->int decltype(func_cint_r()) a2 = x; //a2->const int &
decltype(func_cint_rr()) b2 = ; //b2->const int &&
decltype(func_cint) c2 = ; //c2->int
decltype(func_foo()) ff = Foo(); //ff->const Foo
这里需要注意的是,C2的推导是int而不是const int,这是因为函数的返回值int是个纯右值,对于纯右值而言,只有类类型可以携带cv限定符,除此之外一般忽略掉。所以func_foo推导出来的ff是const Foo。
3.2.3 带括号的表达式和加法运算表达式
struct Foo{ int x;};
const Foo foo = Foo(); decltype(foo.x) a = ; //a->int
decltype((foo.x)) b = a; //const int & int n = , m = ;
decltype(n+m) c = ; //c->int
decltype(n+=m) d = c; //d->int &
这里需要注意的是,b的推导,括号表达式中的foo.x是一个左值,所以decltype的结果是一个左值引用,foo的定义是const Foo,所以foo.x是一个const int类型,所以b是一个const int &;
n+m返回的是一个右值,所以结果是int,n+=m返回一个左值,所以推导出d是int &。
3.3 decltype实际应用
decltype多出现在泛型变成中,我们来看一个例子,假如我们编写一个泛型类:
template<class ContainerT>
class Foo
{
typename ContainerT::iterator it; //类型的定义可能有问题
public:
void func(ContainerT &container)
{
it = container.begin();
}
} int main(void)
{
typedef const std::vector<int> container_t; container_t arr; Foo<container_t> foo;
foo.func(arr); return ;
}
问题很明显,当我们传入一个const容器的时候,我们的定义的iterator不适用,编译器会报错,所以当传入const容器的时候,我们应该使用const_iterator。
如果在C++98/03要解决这样的问题,我们必须增加一个泛型类:
template<class ContainerT>
class Foo<const ContainerT >
{
typename ContainerT::const_iterator it; //类型的定义可能有问题
public:
void func(const ContainerT &container)
{
it = container.begin();
}
}
这样虽然说可以解决问题,但是太麻烦,Foo的其他代码也不得不重新写一次,如果我们用decltype来解决这个问题:
template<class ContainerT>
class Foo
{
decltype(ContainerT().begin()) it; //类型的定义可能有问题
public:
void func(ContainerT &container)
{
it = container.begin();
}
}
decltype也经常用在通过变量表达式抽取变量类型上:
vector<int> v; decltype(v)::value_type I = ;
冗长的代码中,我们只需要关心变量本身,不需要关心它的具体类型,如例子,我们只需要之道v是一个容器,可以提取value_type就OK,而不是到处都需要出现vector<int>这种精确的类型名称。
4、 auto和decltype混编
在泛型编程中,通过auto和decltype的混编来提升灵活性。通过参数的运算来获得返回值类型。
template<typename R, typename T, typename U>
R add(T t, U u)
{
return t + u;
} int a = ;
float b = 2.0; auto c = add<decltype(a + b)>(a, b);
我们不关心a+b类型是什么,通过decltype(a+b)来推导返回值类型。我们还可以通过add函数的定义来获得返回值类型。
template<typename T, typename U>
decltype(t+u) add(T t, U u) //error :t、u尚未定义
{
return t + u;
}
c++的返回值是前置语法,在返回值的时候参数变量都还不存在,所以这样是编译不通过的,我们可以通过构造函数来进行推导:
template<typename T, typename U>
decltype(T() + U()) add(T t, U u)
{
return t + u;
}
但是这样也有一个问题,T、U类型可能是没有无参构造函数,我们可以进行改善:
template<typename T, typename U>
decltype((*(T*)) + (*(U*))) add(T t, U u)
{
return t + u;
}
这样可以成功的使用decltype来完成返回值的推导,但是代码可读性比较差,并增加使用难度。
c++11增加了返回类型后置语法,讲decltype和auto结合起来完成返回值类型的推导。我们再来改善上面的add函数:
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}
我们再来看一个例子:
int &foo(int &i);
float foo(float &f); template<typename T>
auto func(T &val) -> decltype(foo(val))
{
return foo(val);
}
使用decltype结合返回值后置语法很容易推导出foo(val)可能出现的返回值,并用到func上。返回值类型后置语法,是为了解决函数返回值类型依赖与参数而导致难以确定返回值类型的问题,可以很清晰的描述返回值类型,而不是c++98/03那样晦涩难懂的语法来解决问题。
C11简洁之道:类型推导的更多相关文章
- C11简洁之道:lambda表达式
1. 定义 lambda表达式是C++11非常重要也是很常用的特性之一,来源于函数式编程的概念,也是现代编程语言的一个特点.它有如下特点: 声明式编程风格:就地匿名定义目标函数或者函数,不需要额外写 ...
- C11简洁之道:循环的改善
1. for循环的新用法 在C++98/03中,通过for循环对一个容器进行遍历,一般有两种方法,常规的for循环,或者使用<algorithm>中的for_each方法. for循环遍 ...
- C11简洁之道:模板改进
1. 右尖括号 我们在C++98/03中使用泛型编程的时候,经常遇到“>>”被当作右移操作符,而不是模板参数的结尾.假如我们有如下代码: template <typename T& ...
- C11简洁之道:tupe元祖
tuple元组是一个固定大小不同类型的值的集合,是泛化的std::pair.我们也可以把它当作一个通用的结构体来使用,不需要创建结构体有获取结构体特征,在某些情况可以取代结构体,使程序更简洁.直观. ...
- C11简洁之道:初始化改进
1. C++98/03初始化 我们先来总结一下C++98/03的各种不同的初始化情况: //普通数组 ] = {, , }; //POD(plain old data) struct A { int ...
- C11简洁之道:函数绑定
1. 可调用对象 在C++中,有“可调用对象”这么个概念,那么什么是调用对象呢?有哪些情况?我们来看看: 函数指针: 具有operator()成员函数的类对象(仿函数): 可以被转换为函数指针的类对 ...
- 《Clean Code》 代码简洁之道
作者介绍 原文作者: Robert C. Martin, Object Mentor公司总裁,面向对象设计.模式.UML.敏捷方法学和极限编程领域的资深顾问,是<敏捷软件开发:原则.模式.与实践 ...
- JavaScript 代码简洁之道
摘要: 可以说是<Clean Code>的JS代码示例了,值得参考. 原文:JavaScript 代码简洁之道 作者:缪宇 Fundebug经授权转载,版权归原作者所有. 测试代码质量的唯 ...
- Effective Modern C++翻译(2)-条款1:明白模板类型推导
第一章 类型推导 C++98有一套单一的类型推导的规则:用来推导函数模板,C++11轻微的修改了这些规则并且增加了两个,一个用于auto,一个用于decltype,接着C++14扩展了auto和dec ...
随机推荐
- Windows环境下的TensorFlow安装过程
安装环境 Windows8.1 python3.5.x(TensorFlow only supports version 3.5.x of Python on Windows) pip 9.0.1 t ...
- P4编程环境搭建遇到的问题与解决方法
在经历了无数的折腾之后,算是折腾,最后采用的是陈翔学长的脚本加上可爱的shell调整装好的. 链接:p4Install 也许是ubuntu18.04的问题,也有可能是我自己把这个系统折腾的有点杂乱的原 ...
- opencart
1. Deleting english language, what happens? Disable English tab , category and products 1) Fir ...
- TCP系列07—连接管理—6、TCP连接管理的状态机
经过前面对TCP连接管理的介绍,我们本小节通过TCP连接管理的状态机来总结一下看看TCP连接的状态变化 一.TCP状态机整体状态转换图(截取自第二版TCPIP详解) 二.TCP连接建立 ...
- BAT批处理(六)
字符串处理 批处理有着具有非常强大的字符串处理能力,其功能绝不低于C语言里面的字符串函数集.批处理中可实现的字符串处理功能有:截取字符串内容.替换字符串特定字段.合并字符串.扩充字符串等功能.下面对这 ...
- JAVA IDE IntelliJ IDEA使用简介(三)—之你不能忘记的快捷键
IDEA有许多的快捷键来帮助你更便捷的编写代码,以下列出的快捷键(默认情况下,你还没有定制你的快捷键)是工作中经常需要使用到的,请牢记 快捷键 描述 备注 Alt+F1 视图切换 切换当前工作文件的视 ...
- SpringBoot2.0(三) 文件上传
SpringBoot中发起文件上传示例: /** * 文件上传 * @param multipartFile * @param path * @return */ @RequestMapping(va ...
- 第29天:js-数组添加删除、数组和字符串相互转换
一.添加数组var arr=[1,3,5];arr.push(7,9);//添加7和9到数组arr后面,得到[1,3,5,7,9]1.push();可向数组末尾添加一个或多个元素,并返回新的长度.2. ...
- symbol lookup error *** , undefined symbol 错误
在重装samba过程后遇到一些问题,使用 gdb 时产生报错: gdb: symbol lookup error: gdb: undefined symbol: PyUnicodeUCS2_FromE ...
- apt-key 命令
学习参照网上教程在容器中搭建nginx时看到apt-key命令不解,记录一下.一下是 --help中的解释. apt-key命令解释: apt-key add <file> - add t ...