C++:auto与decltype

目录

1.auto类型推导

1.1 auto 类型推导的语法和规则

1.2 auto 的高级用法

1.3 auto 的限制

1.4 auto 的应用

1.4.1 使用 auto 定义迭代器

1.4.2 auto 用于泛型编程

2.decltype类型推导

2.1 decltype 推导规则

2.2 decltype 的实际应用 


1.auto类型推导

1.1 auto 类型推导的语法和规则

        在之前的 C++ 版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。

        C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。

        auto 关键字基本的使用语法如下:

auto name = value; //name 是变量的名字,value 是变量的初始值。

     注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。                   

auto 类型推导的简单例子:

auto n = 10;
auto f = 12.8;
auto p = &n;
auto url = "http://c.biancheng.net/cplus/";

下面我们来解释一下:

  • 第 1 行中,10 是一个整数,默认是 int 类型,所以推导出变量 n 的类型是 int。
  • 第 2 行中,12.8 是一个小数,默认是 double 类型,所以推导出变量 f 的类型是 double。
  • 第 3 行中,&n 的结果是一个 int* 类型的指针,所以推导出变量 p 的类型是 int*。
  • 第 4 行中,由双引号""包围起来的字符串是 const char* 类型,所以推导出变量 url 的类型是 const char*,也即一个常量指针。


我们也可以连续定义多个变量:

int n = 20;
auto *p = &n, m = 99;

        先看前面的第一个子表达式,&n 的类型是 int*,编译器会根据 auto *p 推导出 auto 为 int。后面的 m 变量自然也为 int 类型,所以把 99 赋值给它也是正确的。


        这里我们要注意,推导的时候不能有二义性。在本例中,编译器根据第一个子表达式已经推导出 auto 为 int 类型,那么后面的 m 也只能是 int 类型,如果写作m=12.5就是错误的,因为 12.5 是double 类型,这和 int 是冲突的。

        还有一个值得注意的地方是:使用 auto 类型推导的变量必须马上初始化,这个很容易理解,因为 auto 在 C++11 中只是“占位符”,并非如 int 一样的真正的类型声明。

1.2 auto 的高级用法

         auto 除了可以独立使用,还可以和某些具体类型混合使用,这样 auto 表示的就是“半个”类型,而不是完整的类型。请看下面的代码:

int x = 0;
auto *p1 = &x; //p1 为 int *,auto 推导为 int
auto p2 = &x; //p2 为 int*,auto 推导为 int*
auto &r1 = x; //r1 为 int&,auto 推导为 int
auto r2 = r1; //r2 为 int,auto 推导为 int

下面我们来解释一下:

  • 第 2 行代码中,p1 为 int* 类型,也即 auto * 为 int *,所以 auto 被推导成了 int 类型。
  • 第 3 行代码中,auto 被推导为 int* 类型,前边的例子也已经演示过了。
  • 第 4 行代码中,r1 为 int & 类型,auto 被推导为 int 类型。
  • 第 5 行代码是需要重点说明的,r1 本来是 int& 类型,但是 auto 却被推导为 int 类型,这表明当=右边的表达式是一个引用类型时,auto 会把引用抛弃,直接推导出它的原始类型。


接下来,我们再来看一下 auto 和 const 的结合

int x = 0;
const auto n = x; //n 为 const int ,auto 被推导为 int
auto f = n; //f 为 const int,auto 被推导为 int(const 属性被抛弃)
const auto &r1 = x; //r1 为 const int& 类型,auto 被推导为 int
auto &r2 = r1; //r1 为 const int& 类型,auto 被推导为 const int 类型

下面我们来解释一下:

  • 第 2 行代码中,n 为 const int,auto 被推导为 int。
  • 第 3 行代码中,n 为 const int 类型,但是 auto 却被推导为 int 类型,这说明当=右边的表达式带有 const 属性时, auto 不会使用 const 属性,而是直接推导出 non-const 类型。
  • 第 4 行代码中,auto 被推导为 int 类型,这个很容易理解,不再赘述。
  • 第 5 行代码中,r1 是 const int & 类型,auto 也被推导为 const int 类型,这说明当 const 和引用结合时,auto 的推导将保留表达式的 const 类型。

最后我们来简单总结一下 auto 与 const 结合的用法:

  • 当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;
  • 当类型为引用时,auto 的推导结果将保留表达式的 const 属性。
1.3 auto 的限制

        前面介绍推导规则的时候我们说过,使用 auto 的时候必须对变量进行初始化,这是 auto 的限制之一。那么,除此以外,auto 还有哪些其它的限制呢?

1) auto 不能在函数的参数中使用。
        这个应该很容易理解,我们在定义函数的时候只是对参数进行了声明,指明了参数的类型,但并没有给它赋值,只有在实际调用函数的时候才会给参数赋值;而 auto 要求必须对变量进行初始化,所以这是矛盾的。


2) auto 不能作用于类的非静态成员变量(也就是没有 static 关键字修饰的成员变量)中


3) auto 关键字不能定义数组,比如下面的例子就是错误的:

char url[] = "http://c.biancheng.net/";
auto  str[] = url;  //arr 为数组,所以不能使用 auto


4) auto 不能作用于模板参数,请看下面的例子:

template <typename T>
class A{
//TODO:
};

int main(){
A<int> C1;
A<auto> C2 = C1; //错误
return 0;
}
1.4 auto 的应用

        说了那么多 auto 的推导规则和一些注意事项,那么 auto 在实际开发中到底有什么应用呢?下面我们列举两个典型的应用场景。

1.4.1 使用 auto 定义迭代器

        auto 的一个典型应用场景是用来定义 stl 的迭代器。

        我们在使用 stl 容器的时候,需要使用迭代器来遍历容器里面的元素;不同容器的迭代器有不同的类型,在定义迭代器时必须指明。而迭代器的类型有时候比较复杂,书写起来很麻烦,请看下面的例子:

#include <vector>
using namespace std;

int main(){
    vector< vector<int> > v;
    vector< vector<int> >::iterator i = v.begin();
    return 0;
}

        可以看出来,定义迭代器 i 的时候,类型书写比较冗长,容易出错。然而有了 auto 类型推导,我们大可不必这样,只写一个 auto 即可。

        修改上面的代码,使之变得更加简洁:

#include <vector>
using namespace std;

int main(){
    vector< vector<int> > v;
    auto i = v.begin();  //使用 auto 代替具体的类型
    return 0;
}

        auto 可以根据表达式 v.begin() 的类型(begin() 函数的返回值类型)来推导出变量 i 的类型。

1.4.2 auto 用于泛型编程

         auto 的另一个应用就是当我们不知道变量是什么类型,或者不希望指明具体类型的时候,比如泛型编程中。我们接着看例子:

#include <iostream>
using namespace std;

class A{
public:
    static int get(void){
        return 100;
    }
};

class B{
public:
    static const char* get(void){
        return "http://c.biancheng.net/cplus/";
    }
};

template <typename T>
void func(void){
    auto val = T::get();
    cout << val << endl;
}

int main(void){
    func<A>();
    func<B>();

    return 0;
}

运行结果:

        本例中的模板函数 func() 会调用所有类的静态函数 get(),并对它的返回值做统一处理,但是 get() 的返回值类型并不一样,而且不能自动转换。这种要求在以前的 C++ 版本中实现起来非常的麻烦,需要额外增加一个模板参数,并在调用时手动给该模板参数赋值,用以指明变量 val 的类型。

        但是有了 auto 类型自动推导,编译器就根据 get() 的返回值自己推导出 val 变量的类型,就不用再增加一个模板参数了。

        下面的代码演示了不使用 auto 的解决办法:

#include <iostream>
using namespace std;

class A{
public:
    static int get(void){
        return 100;
    }
};

class B{
public:
    static const char* get(void){
        return "http://c.biancheng.net/cplus/";
    }
};

template <typename T1, typename T2>  //额外增加一个模板参数 T2
void func(void){
    T2 val = T1::get();
    cout << val << endl;
}

int main(void){
    //调用时也要手动给模板参数赋值
    func<A, int>();
    func<B, const char*>();

    return 0;
}

2.decltype类型推导

        decltype 是 C++11 新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。不了解 auto 用法的读者请先认真看第一点《auto类型推导》。

decltype 是“declare type”的缩写,译为“声明类型”。

        既然已经有了 auto 关键字,为什么还需要 decltype 关键字呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。

        auto decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:

auto varname = value;
decltype(exp) varname = value;

其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。

        auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。

        另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式:

decltype(exp) varname;

exp 注意事项

        原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void;例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。

C++ decltype 用法举例:

int a = 0;
decltype(a) b = 1; //b 被推导成了 int
decltype(10.8) x = 5.5; //x 被推导成了 double
decltype(x + 100) y; //y 被推导成了 double

可以看到,decltype 能够根据变量、字面量、带有运算符的表达式推导出变量的类型。读者请留意第 4 行,y 没有被初始化。

2.1 decltype 推导规则

        上面的例子让我们初步感受了一下 decltype 的用法,但你不要认为 decltype 就这么简单,它的玩法实际上可以非常复杂。当程序员使用 decltype(exp) 获取类型时,编译器将根据以下三条规则得出结果:

  • 如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。
  • 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
  • 如果 exp 是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。

        为了更好地理解 decltype 的推导规则,下面来看几个实际的例子。

【实例1】exp 是一个普通表达式:

#include <string>
using namespace std;

class Student{
public:
    static int total;
    string name;
    int age;
    float scores;
};

int Student::total = 0;

int  main(){
    int n = 0;
    const int &r = n;
    Student stu;

    decltype(n) a = n;  //n 为 int 类型,a 被推导为 int 类型
    decltype(r) b = n;     //r 为 const int& 类型, b 被推导为 const int& 类型
    decltype(Student::total) c = 0;  //total 为类 Student 的一个 int 类型的成员变量,c 被推导为 int 类型
    decltype(stu.name) url = "http://c.biancheng.net/cplus/";  //total 为类 Student 的一个 string 类型的成员变量, url 被推导为 string 类型

    return 0;
}

         这段代码很简单,按照推导规则 1,对于一般的表达式,decltype 的推导结果就和这个表达式的类型一致。

【实例2】exp 为函数调用:

//函数声明
int& func_int_r(int, char);  //返回值为 int&
int&& func_int_rr(void);  //返回值为 int&&
int func_int(double);  //返回值为 int

const int& fun_cint_r(int, int, int);  //返回值为 const int&
const int&& func_cint_rr(void);  //返回值为 const int&&

//decltype类型推导
int n = 100;
decltype(func_int_r(100, 'A')) a = n;  //a 的类型为 int&
decltype(func_int_rr()) b = 0;  //b 的类型为 int&&
decltype(func_int(10.5)) c = 0;   //c 的类型为 int

decltype(fun_cint_r(1,2,3))  x = n;    //x 的类型为 const int &
decltype(func_cint_rr()) y = 0;  // y 的类型为 const int&&

需要注意的是,exp 中调用函数时需要带上括号和参数,但这仅仅是形式,并不会真的去执行函数代码。

【实例3】exp 是左值,或者被( )包围:

using namespace std;

class Base{
public:
    int x;
};

int main(){
    const Base obj;

    //带有括号的表达式
    decltype(obj.x) a = 0;  //obj.x 为类的成员访问表达式,符合推导规则一,a 的类型为 int
    decltype((obj.x)) b = a;  //obj.x 带有括号,符合推导规则三,b 的类型为 int&。

    //加法表达式
    int n = 0, m = 0;
    decltype(n + m) c = 0;  //n+m 得到一个右值,符合推导规则一,所以推导结果为 int
    decltype(n = n + m) d = c;  //n=n+m 得到一个左值,符号推导规则三,所以推导结果为 int&

    return 0;
}

        这里我们需要重点说一下左值和右值:左值是指那些在表达式执行结束后依然存在的数据,也就是持久性的数据;右值是指那些在表达式执行结束后不再存在的数据,也就是临时性的数据。有一种很简单的方法来区分左值和右值,对表达式取地址,如果编译器不报错就为左值,否则为右值。

2.2 decltype 的实际应用 

         auto 的语法格式比 decltype 简单,所以在一般的类型推导中,使用 auto 比使用 decltype 更加方便,你可以查看第一点《auto类型推导》有很多类似的例子,本点仅演示只能使用 decltype 的情形。

        我们知道,auto 只能用于类的静态成员,不能用于类的非静态成员(普通成员),如果我们想推导非静态成员的类型,这个时候就必须使用 decltype 了。下面是一个模板的定义:

#include <vector>
using namespace std;

template <typename T>
class Base {
public:
    void func(T& container) {
        m_it = container.begin();
    }

private:
    typename T::iterator m_it;  //注意这里
};

int main()
{
    const vector<int> v;
    Base<const vector<int>> obj;
    obj.func(v);

    return 0;
}

 单独看 Base 类中 m_it 成员的定义,很难看出会有什么错误,但在使用 Base 类的时候,如果传入一个 const 类型的容器,编译器马上就会弹出一大堆错误信息。原因就在于,T::iterator并不能包括所有的迭代器类型,当 T 是一个 const 容器时,应当使用 const_iterator。


        要想解决这个问题,在之前的 C++98/03 版本下只能想办法把 const 类型的容器用模板特化单独处理,增加了不少工作量,看起来也非常晦涩。但是有了 C++11 的 decltype 关键字,就可以直接这样写:

template <typename T>
class Base {
public:
    void func(T& container) {
        m_it = container.begin();
    }

private:
    decltype(T().begin()) m_it;  //注意这里
};

看起来是不是很清爽?

注意,有些低版本的编译器不支持T().begin()这种写法,以上代码我在 VS2019 下测试通过,在 VS2015 下测试失败。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

摸鱼特长生.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值