1. 静态类型,动态类型和类型推导

在编程语言分类中,C/C++ 常常被认为是静态类型的语言。而有的编程语言则号称是“动态类型”的,比如 python。通常情况下,“静”和“动”的区别是非常直观的。我们看看下面这段简单的 python2 代码:

1
2
name=‘world\n’
print 'hello, ' %name

这段代码中 python 中的一个 hellowworld 的实现。这就是编程语言中的“动态类型”,在运行时来进行类型检查,而 C++ 中类型检查是在编译阶段。动态类型语言能做到在运行时决定类型,主要归功于一技术,这技术是类型推导

事实上,类型推导也可以用于静态类型语言中。比如上面的 python 代码中,如果按照 C/C++ 程序员的思考方式,“world\n” 表达式应该可以返回一个临时的字符串,所以即使 name 没有进行声明,我们也能轻松低推导出 name 的类型应该是一个字符串类型。在 C11 中,这个想法得到了实现。C11 中类型推导的实现之一就是重定义 auto 关键字,另一个实现是 decltype

我们可以使用 C++11 方式来书写刚才的 python 的代码

1
2
3
4
5
6
#include <iostream>
using namespace std;
int main() {
auto name=‘world\n’
cout<<"hello "<<name<<endl;
}

这里使用 auto 关键字来要求编译器对变量 name 的类型进行了自动推导。这里编译器根据它的初始化表达式的类型,推导出 name 的类型为 char*。事实上,atuo关键字在早期的 C/C++ 标准中有着完全不同的含义。声明时使用 auto 修饰的变量,按照早期 C/C++ 标准的解释,是具有自动存储期的局部变量。不过该关键字几乎无人使用,因为一般函数内没有声明为 static 的变量总是具有自动存储期的局部变量。auto 声明变量的类型必须又编译器在编译时期推导而得。

通过以下例子来了解以下 auto 类型推导的基本用法:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;
int main() {
double foo();
auto x = 1;
auto y = foo();
struct m { int i; }str;
auto str1 = str;
auto z; // error
z=x;
}

以上变量 x 被初始化为 1,因为字面变量1的类型是 const int,所以编译器推导出x的类型应该为 int(这里const类型限制符被去掉了,后面会解释)。同理在变量 y 的定义中,auto 类型的 y 被推导为 double 类型;而在 auto str1 的定义中,其类型被推导为 struct m。这里的 z,使用 auto 关键字来声明,但是不立即对其进行定义,此时编译器则会报错。这跟通过其他关键字(除去引用类型的关键字)先声明后定义的变量的使用规则是不同的。auto 声明的变量必须被初始化,以使编译器能够从其初始化表达式中推导出其类型。这个意义上,auto 并非一种类型声明,而是一个类型声明时的“占位符”,编译器在便已是亲会将 auto 替代为变量实际的类型。

2. auto 的优势

2.1 更方便使用 STL

直观地,auto 推导的一个最大的优势在于拥有初始化表达式的复杂类型变量声明时简化代码。由于 C++ 的发展,变量类型变得越来越复杂。但是很多时候,名字空间、模板成为类型的一部分,导致了程序员在使用库的时候如履薄冰。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <string>
#include <vector>
using namespace std;
void loopover(vector<string> &vs) {
vector<string>::iterator i = vs.begin();
for( ; i < vs.end(); i++) {
...
}
// new version
void loopover(std::vector<std::string>&vs) {
for( auto i = vs.begin(); i < vs.end(); i++) {
...
}

使用 vector::iterator 来定义 i 是 C++ 常用的良好的习惯,但是这样长的声明带来了代码可读性的困难,因此引入 auto,使代码可读性增加。并且使用 STL 将会变得更加容易

2.2 类型转换

可以避免类型声明时的麻烦而且避免类型声明时的错误。事实上,在 C/C++ 中,存在着很多隐式或者是用户自定义类型的转换规则(比如整型与字符型进行加法运算后,表达式返回整型,这是一条隐式规则)。这些规则并非容易记忆,尤其是在用户自定义很多操作符以后,这个时候 auto 就有用户之地了。看一下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// file 1
class PI{
public:
const float val = 3.1415927f;
double operator* (float v) {
return (double) val * v;
}
}

// file 2
int main() {
float radius = 1.7e10;
PI pi;
auto circumference = 2 * (pi * radius);
}

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

!!!但是auto不能解决所有的精度问题。下面例子:

1
2
3
4
5
6
7
8
int main() {
unsigned int a = 4294967295; //最大的unsigned int值
unsigned int b = 1
auto c = a + b; // 溢出
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}

上面代码中,程序员希望通过声明变量 c 为 auto 就能解决 a+b 溢出的问题。而实际上由于 a+b 返回的依然是 unsigned int 的值,姑且 c 的类型依然被推导为 unsigned int,auto 并不能帮上忙。这个跟动态类型语言中数据自动进行拓展的特性还是不一样的。

2.3 有利于泛型编程

回到上面 class PI 的例子,这里假设 PI 的作者改动了 PI 的定义,比如讲operator* 返回值变为 long double,此时,main 函数并不需要修改,因为 auto 会“自适应”新的类型。同理,对于不同平台上的二次维护,auto 也会带来一些“泛型”的好处。这里我们以 strlen 函数为例,在 32 位编译环境下,strlen 返回的为一个 4 字节的整型,在 64 位的编译环境下,strlen 会返回一个 8 字节的整型。即使系统库中 为其提供了 size_t 类型来支持多平台间的代码共享支持,但是使用 auto 关键字我们同样可以达到代码跨平台的效果。

 auto var = strlen("hello world").

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

当 auto 应用于模板的定义中,其"自适应"性会得到更加充分的体现。我们可以看看以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
float d = 2.3f;
auto e = Sum<int,long>(a,b); //e的类型被推导为long
auto f = Sum<float,float>(c,d); //s的类型被推导为float
}

上面中 Sum 模板函数接受两个参数。由于 T1,T2 要在模板实例化时才能确定,所以 Sum 中将变量 s 的类型声明为 auto 的。

在函数 main 中我们将模板实例化时。Sum<int,long> 中的 s 变量会被推导为 long 类型,而 Sum<float,float> 中的 s 变量则会被推导为 float。可以看到,auto 与模板一起使用时,其“自适应”特性能够加强 C++ 中泛型的能力。

3. 使用 auto 的注意细节

  • 我们可以使用 valatile,pointer(*),reference(&),rvalue reference(&&) 来修饰 auto

      auto k = 5;
      auto* pK = new auto(k);
      auto** ppK = new auto(&k);
      const auto n = 6;
    
  • 用 auto 声明的变量必须初始化

      auto m; // m should be intialized  
    
  • auto 不能与其他类型组合连用

      auto int p; // 这是旧auto的做法。
    
  • 函数和模板参数不能被声明为 auto

      void MyFunction(auto parameter){} // no auto as method argument
      template<auto T> // utter nonsense - not allowed
      void Fun(T t){}
    
  • 定义在堆上的变量,使用了 auto 的表达式必须被初始化

      int* p = new auto(0); //fine
      int* pp = new auto(); // should be initialized
      auto x = new auto(); // Hmmm ... no intializer
      auto* y = new auto(9); // Fine. Here y is a int*
      auto z = new auto(9); //Fine. Here z is a int* (It is not just an int)
    
  • 因为 auto 是一个占位符,并不是一个数据类型,因此不能用于类型转换或其他一些操作,如 sizeof 和 typeid

      int value = 123;
      auto x2 = (auto)value; // no casting using auto
      auto x3 = static_cast<auto>(value); // same as above 
    
  • 定义在一个 auto 序列的变量必须始终推导成同一类型

      auto x1 = 5, x2 = 5.0, x3='r';  // This is too much....we cannot combine like this
    
  • auto 不能自动推导成 CV-qualifiers(constant&volatile qualifiers),除非被声明为引用类型

      const int i = 99;
      auto j = i;       // j is int, rather than const int
      j = 100           // Fine. As j is not constant
      
      // Now let us try to have reference
      auto& k = i;      // Now k is const int&
      k = 100;          // Error. k is constant
      
      // Similarly with volatile qualifer
    
  • auto 会退化成指向数组的指针,除非被声明为引用

      int a[9];
      auto j = a;
      cout << typeid(j).name() << endl; // This will print int*
      
      auto& k = a;
      cout << typeid(k).name() << endl; // This will print int [9]
    

转载自 https://blog.csdn.net/hushujian/article/details/43196589

====