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 ...
随机推荐
- vue移动音乐app开发学习(三):轮播图组件的开发
本系列文章是为了记录学习中的知识点,便于后期自己观看.如果有需要的同学请登录慕课网,找到Vue 2.0 高级实战-开发移动端音乐WebApp进行观看,传送门. 完成后的页面状态以及项目结构如下: 一: ...
- ACM 第十九天
积性函数 积性函数线性筛,筛素数,u(n),欧拉函数: vis[]=vis[]=,mu[]=,phi[]=; ;i<=N;++i){ ,phi[i]=i-,prime[++cnt]=i; ,k= ...
- lintcode-197-排列序号
197-排列序号 给出一个不含重复数字的排列,求这些数字的所有排列按字典序排序后该排列的编号.其中,编号从1开始. 样例 例如,排列 [1,2,4] 是第 1 个排列. 思路 参考http://www ...
- Perfmon - 脚本自动监控
PerfMon-Windows性能监视器是个好东西,可以辅助我们分析发生问题时间段服务器资源占用情况,但是部署性能计数器确实一个相当麻烦的事情,往往这种枯燥的事别人还做不了,只能由我们这些希望获取到P ...
- WebService部署服务器调试时提示 “测试窗体只能用于来自本地计算机的请求”解决方法
原因:没有开启服务器访问权限! 解决方法: 在web.config的<system.web></system.web>中加入如下配置节内容即可解决 <webService ...
- 第51天:封装可视区域大小函数client
一.client 可视区域 offsetWidth: width + padding + border (披着羊皮的狼) clientWidth: width + ...
- 【Python】python 调用c语言函数
虽然python是万能的,但是对于某些特殊功能,需要c语言才能完成.这样,就需要用python来调用c的代码了具体流程:c编写相关函数 ,编译成库然后在python中加载这些库,指定调用函数.这些函数 ...
- RT-thread v2.1.0修正版
RT-Thread v2.1.0是v2.0.1正式版这个系列的bug修正版.RT-Thread v2.1.0修正的主要内容包括: 这个版本经历的时间比较长,并且原定的一些目标也还未能完成(更全的POS ...
- BZOJ 3507 通配符匹配(贪心+hash或贪心+AC自动机)
首先可以对n个目标串单独进行处理. 对于每个目标串,考虑把模式串按'*'进行划分为cnt段.首尾两段一定得于原串进行匹配.剩下的cnt-2段尽量与最靠左的起点进行匹配. 对于剩下的cnt-2段.每段又 ...
- 【bzoj2073】[POI2004]PRZ 状态压缩dp
题目描述 一只队伍在爬山时碰到了雪崩,他们在逃跑时遇到了一座桥,他们要尽快的过桥. 桥已经很旧了, 所以它不能承受太重的东西. 任何时候队伍在桥上的人都不能超过一定的限制. 所以这只队伍过桥时只能分批 ...