一、C++11新特性:auto类型推导


一、auto类型推导

声明:该笔记是在学习《深入理解C++11》、《C++11/14高级编程 Boost程序库探秘》时做的总结,方便以后巩固复习!

1.1、静态类型、动态类型和类型推导

静态类型:C/C++常被成为静态类型的语言,变量必须被定义;

动态类型:python、Perl、JavaScript语言常被称为动态类型的语言,变量不需要声明就可以被使用。

静态类型和动态类型的区别:是在对变量进行类型检测的时间点;静态类型的类型检测主要发生在编译阶段;动态类型的类型检测主要发生在运行阶段。

动态类型语言变量“拿来就用”的特性依赖的是类型推导技术;事实上类型推导也可以用于静态类型的语言;C++11中类型推导的实现方式就有两种:①、auto,②、decltype;先学习auto关键字!

auto关键字在早期的C/C++标准中的含义:

按照C/C++早期标准,声明时使用auto修饰的变量,是具有自动存储的局部变量;几乎无人使用这种含义,因为一般函数内没有声明为static的变量总是具有自动存储的局部变量。

auto关键字C++11中的含义:

auto不再是一个存储类型指示符(如static、extern为纯粹类型指示符),而是一个新的类型指示符(int、float等是类型指示符)来指示编译器,
auto声明变量的类型必须由编译器在编译时期推导而得。

int main()
{
	double foo();
	auto x = 1; //x的类型为int
	auto y = foo(); //y的类型为double
	struct m 
	{
		int i;
	}str; 
	auto str1 = str;//str1的类型是sturct m
	auto z;//无法推导,不能通过编译
}

auto 声明的变量必须被初始化,以使编译能够从其初始化表达式中推导出其类型。这里可以理解为auto并非一种“类型”,而是一个类型声明时的“占位符”,编译器在编译时会将auto替代为变量实际的类型。

1.2、auto的优势

①、最大优势就是在拥有初始化表达式的复杂类型变量声明时简化代码

由于C++的发展,声明变量类型也变得越来越复杂,很多时候,名字空间、模板成为了类型的一部分,导致程序员在使用库的时候如履薄冰。

#include <string>
#incldue <vector>
void loopover(std::vector<std:string> & vs)
{
	std::vector<std::string>::iterator i = vs.begin(); //可看出在在不使用u命名空间时,使用iterator 需要书写大量代码
	for(; i < vs.end(); i++)
	{
		...
	}
}

用auto的话,代码会的可读性可以成倍增长

#include <string>
#incldue <vector>
void loopover(std::vector<std:string> & vs)
{
	std::vector<std::string>::iterator i = vs.begin(); 
	//使用auto,就不需要书写大量代码
	for(auto  i = vs.begin(); i < vs.end(); i++)
	{
		...
	}
}

②、第二个优势则在于可以免除程序员在一些类型声明时的麻烦,或者避免一些在类型声明时的错误

在C/C++中,存在着很多隐式或者用户自定义的类型转换规则(比如整型与字符型进行加法运算后,表达式返回的是整型,这是一条隐式规则)。这些规则并非很容易记忆,尤其是在用户自定义了很多操作符之后。而这个时候,auto就有用武之地了。

class PI
{
    public:
    	double operator* (float v)
        {
            return (double)val * v;
        }
    	const float val = 3.1415927f;
};

int main()
{
    float radius = 1.7e10;
    PI pi;
    auto circumference = 2 * (pi * radius);
    cout << "circumference = " << circumference << endl;
    return 0;
}
输出:
circumference = 1.06814e+11

这里定义了float型的变量radius(半径)以及一个自定义类型PI变量pi(π值),在计算圆周长的时候,使用了auto类型来定义变量circumference。这里,PI在与float类型数据相乘时,其返回值为double。而PI的定义可能是在其他的地方(头文件里),main函数的程序员可能不知道PI的作者为了避免数据上溢或者精度降低而返回了double类型的浮点数。因此main函数程序员如果使用float类型声明circumference,就可能享受不了PI作者细心设计带来的好处。反之,将circumference声明为auto,则毫无问题,因为编译器已经自动地做了最好的选择。

③、第三个优点就是其“自适应”性能够在一定程度上支持泛型的编程

我们再回到上面代码例子,这里假设改动了PI的定义,如将operator*返回值变为long double,此时,main函数并不需要修改,因为auto会“自适应”新的类型。

同时,对于不同的平台上的代码维护,auto也会带来一些“泛型”的好处。这里我们以strlen函数为例,在32位的编译环境下,strlen返回的为一个4字节的整型,而在64位的编译环境下,strlen会返回一个8字节的整型。虽然系统库为其提供了size_t类型来支持多平台间的代码共享支持,但是使用auto关键字我们同样可以达到代码跨平台的效果。

auto v = strlen("hello world!")

由于size_t的适用范围往往局限于中定义的函数,auto的适用范围明显更为广泛。

当auto应用于模板的定义中,其“自适应”性会得到更加充分的体现。如:

#include <iostream>
using namespace std;

template<typename T1, typename T2>
double Sum(T1 & t1, T2 & t2)
{
    auto s = t1 + t2;
    return s;
}

int main()
{
    int a = 3;
    long b = 5;
    float c = 1.0f, d = 2.3f;
    auto e = Sum<int,long>(a,b);//s的类型被推导为long
    auto f = Sum<float,float>(c,d);//s的类型被推导为float
	cout << e << endl;
	cout << f << endl;
	return 0;
}
输出:
8
3.3

在上面程序中,由于类型T1、T2要在模板实例化时才能确定,所以在Sum中将变量s的类型声明为auto的。在函数main中我们将模板实例化时,Sum<int,long>中的s变量会被推导为long类型,而Sum<float, float>中的s变量则会被推导为float。可以看到,auto与模板一起使用时,其“自适应”特性能够加强C++中“泛型”的能力。不过在这个例子中,由于总是返回double类型的数据,所以Sum模板函数的适用范围还是受到了一定的限制。

④、在宏定义中,避免出现性能问题

#include <iostream>
using namespace std;

#define MAX1(a, b) ((a) > (b)) ? (a) : (b)
#define MAX2(a, b) ({
        auto _a = (a);
        auto _b = (b);
        (_a > _b) ? _a : _b;})
        
 int main()
 {
 	int m1 = MAX1(1*2*3*4, 5+6+7+8);
 	int m2 = MAX2(1*2*3*4, 5+6+7+8);
 	cout << m1 << endl;
 	cout << m2 << endl;
	return 0; 
 }

定义了两种类型的宏MAX1和MAX2。两者作用相同,都是求a和b中较大者并返回。前者采用传统的三元运算符表达式,这可能会带来一定的性能问题。因为a或者b在三元运算符中都出现了两次,那么无论是取a还是取b,其中之一都会被运算两次。而在MAX2中,我们将a和b都先算出来,再使用三元运算符进行比较,就不会存在这样的问题了。

在传统的C++98标准中,由于a和b的类型无法获得,所以我们无法定义MAX2这样高性能的宏。而新的标准中的auto则提供了这种可行性。

1.3、auto使用时注意事项

①、auto类型指示符与指针和引用之间的关系

int x = 1;
int * y = &x;
double foo();
int & bar();
auto * a = &x; // int*
auto & b = x;// int&
auto c = y;// int*
auto * d = y; // int*
//auto * e = &foo();//编译失败,指针不能指向一个临时变量
//auto & f = foo();//编译失败,nonconst的左值引用不能和一个临时变量绑定
auto g = bar();// int
auto & h = bar();// int&

变量a、c、d的类型都是指针类型,且都指向变量x。实际上对于a、c、d三个变量而言,声明其为auto *或auto并没有区别。

而如果要使得auto声明的变量是另一个变量的引用,则必须使用auto &,如同本例中的变量b和h一样。

②、auto与volatile和const之间也存在着一些相互的联系

volatile和const代表了变量的两种不同的属性:易变的和常量的。

在C++标准中,它们常常被一起叫作cv限制符(cv-qualifier)。鉴于cv限制符的特殊性,C++11标准规定auto可以与cv限制符一起使用,不过声明为auto的变量并不能从其初始化表达式中“带走”cv限制符

double foo();
float * bar();
const auto a = foo(); //a:const double
const auto & b = foo(); //b:const double&
volatile auto * c = bar(); //c:volatile float*
auto d = a; //d:double
auto & e = e; //e:const double &
auto f = c; //f:float *
volatile auto & g = c; //g:volatile float * &

可以看出通过非cv限制的类型初始化一个cv限制的类型,如变量a、b、c所示。不过通过auto声明的变量d、f却无法带走a和f的常量性或者易失性。这里的例外还是引用,可以看出,声明为引用的变量e、g都保持了其引用的对象相同的属性(事实上,指针也是一样的)。

③、auto可以用来声明多个变量的类型,不过这些变量的类型必须相同

如果这些变量的类型不相同,编译器则会报错。事实上,用auto来声明多个变量类型时,只有第一个变量用于auto的类型推导,然后推导出来的数据类型被作用于其他的变量。

auto x = 1, y = 2;
//m是一个指向const int类型变量的指针,n是一个int类型的变量
const auto* m = &x, n = 1;
//auto i = 1, j = 3.14f; //编译失败
auto o = 1,&p = o,*q = &p; //从左向右推导

使用auto声明了两个类型相同变量x和y,并用逗号进行分隔,这可以通过编译。而在声明变量i和j的时候,按照我们所说的第一变量用于推导类型的规则,那么由于x所推导出的类型是int,那么对于变量j而言,其声明就变成了int j =3.14f,这无疑会导致精度的损失。而对于变量m和n,就变得非常有趣,这里似乎是auto被替换成了int,所以m是一个int *指针类型,而n只是一个int类型。同样的情况也发生在变量o、p、q上,这里o是一个类型为int的变量,p是o的引用,而q是p的指针。auto的类型推导按照从左往右,且类似于字面替换的方式进行。事实上,标准里称auto是一个将要推导出的类型的“占位符”(placeholder)。这样的规则无疑是直观而让人略感意外的。当然,为了不必要的繁琐记忆,程序员可以选择每一个auto变量的声明写成一行(有些观点也认为这是好的编程规范)。

④、只要能够进行推导的地方,C++11都为auto指定了详细的规则,保证编译器能够正确地推导出变量的类型

包括C++11新引入的初始化列表,以及new,都可以使用auto关键字

 #include <initializer_list>
 auto x = 1;
 auto x1(1);
 auto y {1};      // 使用初始化列表的auto      	auto z = new auto(1);    // 可以用于new       

auto变量y的初始化使用了初始化列表,编译器可以保证y的类型推导为int。而z指针所指向的堆变量在分配时依然选择让编译器对类型进行推导,同样的,编译器也能够保证这种方式下类型推导的正确性。

⑤、不过auto也不是万能的,受制于语法的二义性,或者是实现的困难性,auto往往也会有使用上的限制

  #include <vector>      
  using namespace std;      
  //void fun(auto x =1){}  // 1: auto函数参数,无法通过编译
  struct str{
  	//auto var = 10;    // 2: auto非静态成员变量,无法通过编译
    };
  int main() {
  char x[3];
  auto y = x;
 // auto z[3] = x; // 3: auto数组,无法通过编译    // 4: auto模板参数(实例化时),无法通过编译
  vector<auto> v = {1};
  }  

①、对于函数fun来说,auto不能是其形参类型。可能读者感觉对于fun来说,由于其有默认参数,所以应该推导fun形参x的类型为int型。但事实却无法符合大家的想象。因为auto是不能做形参的类型的。如果程序员需要泛型的参数,还是需要求助于模板。

②、对于结构体来说,非静态成员变量的类型不能是auto的。同样的,由于var定义了初始值,读者可能认为auto可以推导str成员var的类型为int的。但编译器阻止auto对结构体中的非静态成员进行推导,即使成员拥有初始值。

③、声明auto数组。我们可以看到,main中的x是一个数组,y的类型是可以推导的,而声明auto z[3]这样的数组同样会被编译器禁止。

④、在实例化模板的时候使用auto作为模板参数,如main中我们声明的vector v。虽然读者可能认为这里一眼而知是int类型,但编译器却阻止了编译。

欢迎关注公众号:Kevin的嵌入式学习站
在这里插入图片描述

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>