您的位置:首页 > 编程语言 > C语言/C++

C++11新特性之auto&decltype

2016-12-29 16:39 597 查看
C++11中引入了auto&decltype关键字实现类型推导,通过这两个字可以方便的获取复杂的类型还可以简化代码,提高编码效率。

auto关键字

auto类型推导

C++98/03中一直就有auto关键字了,只不过在C++98/03中auto关键字用于标识具有自动存储期的局部变量,它的作用并不大,在实际的编码过程中我们基本没有用过auto关键字,比如

auto int i = 0;  //在C++98/03中 它的定义和int i = 0;是一样一样的,因此通常为了方便我们都省略了auto关键字,与之相对应的是

static int i = 0; //标识i是一个静态类型,存储在堆的全局数据区。

在C++11中auto关键字被重新利用了,用作类型推导,比如通常我们定义一个变量:

int i = 0; //强类型定义 改用auto关键字的话就可以 auto i= 20;这样定义了,我们就不需要关系对于每个值定义成什么类型,编译器自动会去根据初始化表达式的值的类型去推导变量的具体类型,因此也就意味着,编译器要推导出变量的类型,那么该变量就必须初始化,否则会编译失败。

看下面auto的一些基本用法:

auto x = 5;					//OK:x是int类型
auto pi = new auto(1);		//OK:pi 是int *类型
const auto *v = &x, u = 6;  //OK:V是const int *类型
static auto y = 0.0;		//OK:y是double类型
auto int r;					//error 无法通过编译
auto s;						//error 无法通过编译


使用VC2013 IDE在main返回之前打个断点,可以再VC的local窗口看到变量的类型,如下:



因此,由上可知,auto不能代表一个实际的类型声明,如s编译错误,仅仅是用作类型声明的一个占位符,在编译期间,编译器会根据后面初始化值的具体类型替换auto占位符。

auto的推导规则

下面通过一组例子我们来看一下编译器在编译器间是如何推导出变量的具体类型的。
int x = 0;
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 -> int auto 被推导为int
auto f = e;			//f  -> int auto被推导为int

const auto & g = x; //g ->const int &
auto &h = g;        //h -> const int &




上面的例子可以看出。a,c的推导结果比较见到,b说明b不声明为指针也可以推导出b为指针类型,需要注意的是:
1、d的推导结果说明当表达式是一个引用类型的时候,auto会把引用类型抛弃,直接推导成原始类型int.
2、f的推导结果说明,当表达式是一个带cv限定符时,auto会把cv限定符也就是const抛弃,推导成non-const的int.
3、gh的结果说明,当auto和引用类型想结合时编译器在推导时保留表达式的cv限定符。
因此可以得到如下两条auto的推导规则:
1、当不声明为指针和引用是,auto的推导结果类型将抛弃表达式的引用和CV限定符。如上面的d,f.
2、当声明为指针或引用时,auto推导结果将保留表达式的的cv属性,如上面的g;

auto的限制

请看如下代码示例:

void func(auto a = 1){};  //error: auto不能用于函数形参,哪怕是带默认参数的形参

struct MyStruct
{
auto var = 1;			//error: auto不能修饰类或者结构体中的非静态成员
static auto var2 = 2;
};

template<class T>
struct Bar{};
int _tmain(int argc, _TCHAR* argv[])
{
int arr[10] = { 0 };
auto a = arr;  //OK: a ->int *
auto r[10] = arr;  //error : auto不能定义数组

Bar<int> bar;
Bar<auto> bb = bar;  //error: auto 不能作为模板参数
return 0;
}
由上面例子可以看出,auto的使用限制为:
1、auto不能作为函数参数。
2、auto不能修饰类或者结构体中的非静态成员变量
3、auto不能定义数组
4、auto不能推导出模板参数

需要注意的是在auto a = arr中auto被推导出的类型是int*类型

auto的使用

说了这么多,那么该什么时候使用auto以及如何使用auto呢?
1、在C++1前,我们要使用一个STL容器,要遍历容器内的元素的时候通常会这样写
std::map<int, int> m_map;

//add element to map

std::map<int, int>::iterator it = m_map.begin();
for (; it != m_map.end(); it++)
{
//dosomething
}
通过上面的迭代器,通过迭代器的begin方法其实就已经知道了变量it的类型了,我们却还是要书写一串常常的类型声明才能通过编译,而有了map之后你就可以这样写了:
std::map<int, int> m_map;

//add element to map
for (auto it = m_map.begin(); it != m_map.end(); it++)
{
//dosomething
}
系不系清爽很多,再也不用书写冗长的迭代器类型声明了。

2、很多时候我们不知道给变量定义声明类型,比如下面代码所示:
class A
{
public:
static int Get()
{
return 0;
}

};

class B
{
public:
static char* Get()
{
return "abcdefg";
}
};

template<class T>
void func(void)
{
auto val = T::Get();
//....
}

int _tmain(int argc, _TCHAR* argv[])
{
func<A>();
func<B>();
return 0;
}
在上例中我们希望定义一个泛型函数func,对所有具体静态方法Get的类型T在得到Get结果后做统一的处理,如果不使用auto,就不得不再增加一个模板参数,并在外部调用时手动指定Get的返回值类型。

以上示例仅说明auto的一些基本的使用方法,更多的使用方法需要各位码农在编码中灵活应用。

decltype关键字

如何获取表达式的类型

前面讲的auto,用于通过一个表达式在初始化时期确定变量的具体类型,auto修饰的变量必须初始化,也即是必须得定义变量,如果不需要或者不能定义变量,却想要得到类型该怎么办呢?
C++11新增了一个关键字decltype,用于在编译时期推导出一个表达式的类型,而不用初始化,其语法格式有点像sizeof:
decltype(expr)
于sizeof一样,decltype推导表达式类型也是在编译时期就完成的,并不会真正的计算表达式的值。

下面列出了decltype的一些基本的用法:
int x = 0;
decltype(x) y = 1;   //y -> int
decltype (x + y) z = 0;  //z -> int

const int & i = x;
decltype(i) j = y;  //j -> const  int &

const decltype(z) *p = &z;  // p -> const int *
decltype(z) *pi = &z;     //pi -> int *
decltype(pi) *pp = &pi   //pp -> int **




y,z的结果表明,decltype可以根据表达式直接推导出它的类型本身,这个跟auto很像,可是又是不同,auto必须根据初始化值的类型来推导变量的类型,若想要类型却不想定义变量,auto就不适用了。j表明decltype关键字在类型推导的时候可以保留表达式的引用和CV限定符,不会像auto那样当表达式不是指针和引用时会抛弃引用和CV限定符,p和pi说明decltype可以跟auto一样加上引用指针和CV限定符,,pp则说明当表达式是一个指针的时候,decltype仍然推导出表达式的实际类型,之后再结合pp定义是的指针标记,得到pp是一个实际的二维指针,总之记住一句话,decltype永远推导出表达式的实际类型,不管你定义时是不是加了指针或者引用标记。

decltype的推导规则

decltype(expr)的推导规则如下:
1、expr是标识符,类访问表达式,decltype(expr)和expr的类型保持一致。
2、expr是函数调用,decltype(expr)和返回值类型一致。
3、expr是一个左值,则decltype(expr)是expr类型的左值引用,否则和expr类型一致。
下面分别对上面三种情况依次讨论:
(1)expr是标识符表达式或者是类访问表达式
class Foo
{
public:
static const int number = 0;
int x;
};
int _tmain(int argc, _TCHAR* argv[])
{
int n = 0;
volatile const int &x = n;

decltype(n) a = n;    //a -> int
decltype(x) b = n;    //b -> volatile const int &

decltype(Foo::number) c = 0;  //const int

Foo foo;
decltype(foo.x) d = 0; //d ->int  类访问表达式

return 0;
}



由上面的例子可见,变量a b c保留的表达式的所有属性(包括CV属性和引用属性),根据推导规则1对于表示服表达式,decltype的推导结果和expr保持一致,而d是一个类访问表达式,也符号推导规则1,因此推导结果也和expr标尺一致。

(2)expr是一个函数调用
看下面一个例子:
int& func_r(void);  //返回值类型是一个左值
int && func_rr(void); //返回值类型是一个右值
int func(void);   //返回值类型是一个纯右值

const int& func_cint_r(void);  //左值
const int && func_cint_rr(void);//右值
const int func_cint(void); //纯右值

int _tmain(int argc, _TCHAR* argv[])
{
int x = 0;
decltype(func_r()) a1 = x;    //a1 -> int &
decltype(func_rr()) b1 = 0;   //b1 -> int &&
decltype(func()) c1 = 0;      //c1 -> int

decltype(func_cint_r()) a2 = x;  //a2 -> const int&
decltype(func_cint_rr())  b2 = 0; //b2 -> const int &&
decltype(func_cint()) c2 = 0;     //c2 -> int

return 0;
}




从上面可以看出如果expr是一个函数调用的话,则decltype的推导结果与函数的返回值类型一致,并携带相应的CV限定符。需要注意的是c2的类型是int  而不是const int。这是因为函数返回的是一个纯右值,对于纯右值,只有类类型可以携带CV限定符,其他一般忽略掉CV限定符。

(3)带括号的表达式和加法运算表达式
struct Foo{ int x; };
const Foo foo = Foo();

decltype(foo.x) a = 0;  //a -> int
decltype((foo.x)) b = a;  // b-> const int &

int n = 0, m = 0;
decltype(n + m) c = 0;   //c -> int
decltype(n += m) d = c;  //d -> int &



a/b的结果仅相差一对括号,但是得到的类型确实不同的,a的结果很直接,根据推导规则1,a的类型就是foo.x的类型,然而b的类型不适合推导规则1,2,由于foo.x是一个左值,因此括号表达式也是一个左值,根据推导规则3得到的推导结果是一个左值引用,而由于foo的定义是const Foo,因此decltype的推导结果就是const int &,同样n+=m也是一个左值,因此得到的推导结果是int&。

decltype的实际应用

decltype的应用多出现在泛型编程中,考虑如下的使用场景:
template <typename ContainerT>
class Foo
{
typename ContainerT::iterator _it;
public:
void func(ContainerT& container)
{
it = container.begin();
}
//...
};

int _tmain(int argc, _TCHAR* argv[])
{
typedef const std::vector(int) container_t;
container_t arr;
Foo<container_t> foo;
foo.func(arr);
return 0;
}
单独去看Foo中的it定义,很难看出会出什么错误,但是根据main中程序传入的的是一个const的容器类型,bebin函数返回的是一个const类型的迭代器,而it成员是一个普通的迭代器类型,编译器会弹出一大堆错误信息,要解决上面的问题,在C++98/03中只能通常是增加一个模板特化的模板函数专门用于处理const类型的容器迭代器,这样的方法实在不是一个特别好的办法,const的模板仅仅是为了处理迭代器类型的限制而增加的而Foo的代码却又不得不重写一次,增加了代码冗余不说,代码可读性也会变差,而又了decltype类型推导以后你就可以这样写上面的代码了。
template <typename ContainerT>
class Foo
{
decltype(ContainerT::begin()) _it;
public:
void func(ContainerT& container)
{
it = container.begin();
}
//...
};

int _tmain(int argc, _TCHAR* argv[])
{
typedef const std::vector(int) container_t;
container_t arr;
Foo<container_t> foo;
foo.func(arr);
return 0;
}


这样不仅可以用于普通的迭代器类型也使用与任何一种迭代器类型,不需要去匹配不同的迭代器类型而对模板做特化处理,是不是方便多了?

aoto和decltype结合使用--返回类型后置语法

在泛型编程中,可能需要通过参数的运算来得到返回值的类型,考虑下面的场景:
template <typename R, typename T, typename U>
R add(T t, U u)
{
return t + u;
}

int _tmain(int argc, _TCHAR* argv[])
{
int a = 1; float b = 2.0;
auto c = add<decltype(a + b)>(a, b);
return 0;
}
我们并不关心a+b的类型是啥,只需要通过decltype(a+b)的类型直接得到返回值的类型就可以了,但是上面的使用方法十分不方便吧,我们外部其实并不知道模板函数add中的实际运算是怎么进行了,因此也就不知道add函数返回值应该如何推导,那么在add函数的定义上能不能拿到函数的返回值呢?比如是否可以像下面这样定义呢》?
template < typename T, typename U>
decltype(t+u) add(T t, U u)
{
return t + u;
}
遗憾的是上面的定义是编译不过了,编译错误显示t,u未定义,因为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*)0)+ (*(U*)0) add(T t, U u)
{
return t + u;
}
虽然成功的完成了返回值的推导,但是上述的代码的太过于晦涩,大大增加了返回值类型推导的难度和代码的可读性变的极差。因此在C++11中增加了返回值类型后置(trailing-return-type 又叫跟踪返回类型)语法,将decltype和auto结合完成返回值类型的推导,极大的增加了代码的可读性。
利用返回值类型后置语法改写上面的add函数:
template < typename T, typename U>
auto add(T t, U u) -> decltype(t+u)
{
return t + u;
}

int _tmain(int argc, _TCHAR* argv[])
{
int a = 1; float b = 2.0;
auto c = add(a, b);
return 0;
}


代码可读性系不系变的很好了啊?哈哈

至此,C++中auto和decltype的用法介绍完毕,希望看了这篇文章的读者能够熟练使用auto和decltype,学习编程的最重要的手段就是写代码,不写代码只看的话永远学不会编程。

参考资料:《深入应用C++11 代码优化与工程机应用》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: