现代C++新特性 auto占位符

    文字版PDF文档链接:现代C++新特性(文字版)-C++文档类资源-CSDN下载

 重新定义的auto关键字

严格来说auto并不是一个新的关键字,因为它从C++98标准开始就已经存在了。当时auto是用来声明自动变量的,简单地说,就是拥有自动生命期的变量,显然这是多余的,现在我们几乎不会使用它。于是C++11标准赋予了auto新的含义:声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符。例如:

auto i = 5;                      // 推断为int 
auto str = "hello auto";         // 推断为const char* 
auto sum(int a1, int a2)->int    // 返回类型后置,auto为返回值占位符 
{
    return a1 + a2;
}

在上面的代码中,我们不需要为i和str去声明具体的类型,auto要求编译器自动完成变量类型的推导工作。sum函数中的auto 是一个返回值占位符,真正的返回值类型是int,sum函数声明采用了函数返回类型后置的方法,该方法主要用于函数模板的返回值推导(见第5章)。注意,auto占位符会让编译器去推导变量类型,如果我们编写的代码让编译器无法进行推导,那么使用auto会导致编译失败,例如:

auto i;    // 编译失败
i = 5;

很明显,以上代码在声明变量时没有对变量进行初始化,这使编译器无法确认其具体类型要导致编译错误,所以在使用auto占位符声明变量的时候必须初始化变量。进一步来说,有4点需要引起注意。

1.当用一个auto关键字声明多个变量的时候,编译器遵从由左往右的推导规则,以 左边的表达式推断auto的具体类型:

int n = 5;
auto* pn = &n, m = 10;

在上面的代码中,因为&n类型为int *,所以pn的类型被推导为int *,auto被推导为int,于是m被声明为int类型,可以编译成功。但是如果写成下面的代码,将无法通过编译:

int n = 5; 
auto* pn = &n, m = 10.0;  // 编译失败,声明类型不统一

上面两段代码唯一的区别在于赋值m的是浮点数,这和auto推导类型不匹配,所以编译器通常会给予一条“in a declarator-list 'auto' must always deduce to the same type”报错信息。细心的读者可能会注意到,如果将赋值代码替换为int m = 10.0;,则编译器会进行缩窄转换, 终结果可能会在给出一条警告信息后编译成功,而在使用auto声明变量的情况下编译器是直接报错的。

2.当使用条件表达式初始化auto声明的变量时,编译器总是使用表达能力更强的类型:

auto i = true ? 5 : 8.0;    // i的数据类型为double

在上面的代码中,虽然能够确定表达式返回的是int类型,但是i的类型依旧会被推导为表达能力更强的类型double。

3.虽然C++11标准已经支持在声明成员变量时初始化(见第8 章),但是auto却无法在这种情况下声明非静态成员变量:

struct sometype {
    auto i = 5;    // 错误,无法编译通过 
};

在C++11中静态成员变量是可以用auto声明并且初始化的,不过前提是auto必须使用const限定符:

 struct sometype {
    static const auto i = 5;
};

遗憾的是,const限定符会导致i常量化,显然这不是我们想要的结果。幸运的是,在C++17标准中,对于静态成员变量,auto可以在没有const的情况下使用,例如:

 struct sometype {
    static inline auto i = 5;    // C++17 
};

4.按照C++20之前的标准,无法在函数形参列表中使用auto声明形参(注意,在C++14中,auto可以为lambda表达式声明形参):

 void echo(auto str) // C++20之前编译失败,C++20编译成功
{ 
    //… 
}  

另外,auto也可以和new关键字结合。当然,我们通常不会这么用,例如:

auto i = new auto(5);
auto* j = new auto(5);

这种用法比较有趣,编译器实际上进行了两次推导,第一次是auto(5),auto被推导为int类型,于是new int的类型为int *,再通过int *推导i和j的类型。我不建议像上面这样使用auto,因为它会破坏代码的可读性。在后面的内容中,我们将讨论应该在什么时候避免使用auto关键字。

​​​​​​​ 推导规则

1.如果auto声明的变量是按值初始化,则推导出的类型会忽略 cv限定符。进一步解释为,在使用auto声明变量时,既没有使用引用,也没有使用指针,那么编译器在推导的时候会忽略const和volatile限定符。当然auto本身也支持添加cv限定符:

const int i = 5;
auto j = i;        // auto推导类型为int,而非const int
auto& m = i;       // auto推导类型为const int,m推导类型为const int& 
auto* k = i;       // auto推导类型为const int,k推导类型为const int* 
const auto n = j;  // auto推导类型为int,n的类型为const int

根据规则1,在上面的代码中,虽然i是const int类型,但是因为按值初始化会忽略cv限定符,所以j的推导类型是int而不是const int。而m和k分别按引用和指针初始化,因此其cv属性保留了下来。另外,可以用const结合auto,让n的类型推导为const int。

2.使用auto声明变量初始化时,目标对象如果是引用,则引用属性会被忽略:

int i = 5; 
int& j = i;
auto m = j;    // auto推导类型为int,而非int&

根据规则2,虽然j是i的引用,类型为int&,但是在推导m的时候会忽略其引用。

3.使用auto和万能引用声明变量时(见第6章),对于左值会将auto推导为引用类型:

int i = 5;
auto&& m = i;    // auto推导类型为int& (这里涉及引用折叠的概念) 
auto&& j = 5;    // auto推导类型为int

根据规则3,因为i是一个左值,所以m的类型被推导为int&,

auto被推导为int&,这其中用到了引用折叠的规则。而5是一个右值,因此j的类型被推导为int&&,auto被推导为int。 4.使用auto声明变量,如果目标对象是一个数组或者函数,则 auto会被推导为对应的指针类型:

int i[5];
auto m = i;    // auto推导类型为int* 
int sum(int a1, int a2)
{
    return a1 + a2;
}
auto j = sum   // auto推导类型为int (__cdecl *)(int,int)

根据规则4,虽然i是数组类型,但是m会被推导退化为指针类型,同样,j也退化为函数指针类型。

5.当auto关键字与列表初始化组合时,这里的规则有新老两个版本,这里只介绍新规则(C++17标准)。

直接使用列表初始化,列表中必须为单元素,否则无法编译,auto类型被推导为单元素的类型。

用等号加列表初始化,列表中可以包含单个或者多个元素,auto类型被推导为initializer_list<T>,其中T是元素类型。请注意,在列表中包含多个元素的时候,元素的类型必须相同,否则编译器会报错。

auto x1 = { 1, 2 };      // x1类型为 initializer_list<int>
auto x2 = { 1, 2.0 };    // 编译失败,花括号中元素类型不同 
auto x3{ 1, 2 };         // 编译失败,不是单个元素 
auto x4 = { 3 };         // x4类型为initializer_list<int> 
auto x5{ 3 };            // x5类型为int

在上面的代码中,x1根据规则5(2)被推导为initializer_list<T>,其中的元素都是int类型,所以x1 被推导为initializer_list<int>。同样,x2也应该被推导为initializer_list<T>,但是显然两个元素类型不同,导致编译器无法确定T的类型,所以编译失败。根据规则5(1),x3包含多个元素,直接导致编译失败。x4和x1一样被推导为initializer_ list<int>,x5被推导为单元素的类型 int。

根据上面这些规则,读者可以思考下面的代码,auto会被推导成什么类型呢?

class Base {
public:
    virtual void f()
    {
        cout << "Base::f()" << endl;
    };
};
class Derived : public Base {
public:
    virtual void f() override
    {
        cout << "Derived::f()" << endl;
    };
};
Base* d = new Derived(); 
auto b = *d;
b.f();

以上代码有Derived和Base之间的继承关系,并且Derived重写了Base的f函数。代码使用new创建了一个Derived对象,并赋值于基类的指针类型变量上。读者知道d->f()一定调用的是Derived 的f函数。但是b.f()调用的又是谁的f函数呢?实际上,由于auto b = *d这一句代码是按值赋值的,因此auto会直接推导为Base。代码自然会调用Base的复制构造函数,也就是说Derived被切割成了Base,这里的b.f()终调用Base的f函数。那么进一步发散,如果代码写的是auto &b = *d,结果又会如何呢?这个就交给读者自己验证了。​​​​​​​

​​​​​​​ 什么时候使用auto

合理使用auto,可以让程序员从复杂的类型编码中解放出来,不但可以少敲很多代码,也会大大提高代码的可读性。但是事情总是有它的两面性,如果滥用auto,则会让代码失去可读性,不仅让后来人难以理解,间隔时间长了可能自己写的代码也要研读很久才能弄明白其含义。所以,下面我们来探讨一下,如何合理地使用auto。这里再多说一句,每个人对auto的使用可能有不同的理解,我这里阐述的是自己认为 合适的使用场景。首先简单归纳auto的使用规则。

1.当一眼就能看出声明变量的初始化类型的时候可以使用 auto。

2.对于复杂的类型,例如lambda表达式、bind等直接使用 auto。

对于第一条规则,常见的是在容器的迭代器上使用,例如:

map<string, int> str2int;
// … 填充str2int的代码 
for (map<string, int>::const_iterator it = str2int.cbegin(); it != str2int.cend(); ++it) {

}
// 或者 
for (pair<const string, int>& it : str2int) {

}

上面的代码如果不用auto来声明迭代器,那么我们需要编写map <string, int>::const_iterator和 pair<const string, int>来代替auto,而多出来的代码并不会增强代码的可读性,反而会让代码看起来冗余,因为通常我们一眼就能看明白it的具体类型。请注意,第二个for的it类型是pair<const string, int>,而不是 pair< string, int>,如果写成后者是无法通过编译的。直接使用auto,可以避免上述问题:

map<string, int> str2int;
// … 填充str2int的代码 
for (auto it = str2int.cbegin(); it != str2int.cend(); ++it) {

}
// 或者 
for (auto& it : str2int) {

}

反过来说,如果使用auto声明变量,则会导致其他程序员阅读代码时需要翻阅初始化变量的具体类型,那么我们需要慎重考虑是否适合使用auto关键字。

对于第二条规则,我们有时候会遇到无法写出类型或者过于复杂的类型,或者即使能正确写出某些复杂类型,但是其他程序员阅读起来也很费劲,这种时候建议使用auto来声明,例如lambda表达式:

auto l = [](int a1, int a2) { return a1 + a2; };

这里l的类型可能是一个这样的名称xxx::<lambda_efdefb7231ea076 22630c86251a36ed4>(不同的编译器命名方法会有所不同),我们根本无法写出其类型,只能用auto 来声明。再例如:

int sum(int a1, int a2) 
{ 
    return a1 + a2; 
} 
auto b = bind(sum, 5, placeholders::_1);

这里b的类型为_Binder<_Unforced, int( cdecl &) (int, int), int, const _Ph<1> &>,绝大多数读者看到这种类型时会默契地选择使用auto来声明变量。

​​​​​​​返回类型推导

C++14标准支持对返回类型声明为auto的推导,例如:

auto sum(int a1, int a2) 
{ 
    return a1 + a2; 
}

在上面的代码中,编译器会帮助我们推导sum的返回值,由于a1和a2都是int类型,所以其返回类型也是int,于是返回类型被推导为int类型。请注意,如果有多重返回值,那么需要保证返回值类型是相同的。例如:

auto sum(long a1, long a2)
{
    if (a1 < 0) {
        return 0;          // 返回int类型   
    } else {
        return a1 + a2;    // 返回long类型 
    }
}

以上代码中有两处返回,return 0返回的是int类型,而return a1+a2返回的是long类型,这种不同的返回类型会导致编译失败。

​​​​​​​ lambda表达式中使用auto类型推导

在C++14标准中我们还可以把auto写到lambda表达式的形参中,这样就得到了一个泛型的lambda表达式,例如:

auto l = [](auto a1, auto a2) { return a1 + a2; };
auto retval = l(5, 5.0);

在上面的代码中a1被推导为int类型,a2被推导为double类型,返回值retval被推导为double类型。

让我们看一看lambda表达式返回auto引用的方法:

auto l = [](int& i)->auto& { return i; }; 
auto x1 = 5;
auto& x2 = l(x1);
assert(&x1 == &x2);    // 有相同的内存地址

起初在后置返回类型中使用auto是不允许的,但是后来人们发现,这是唯一让lambda表达式通过推导返回引用类型的方法了。

​​​​​​​ 非类型模板形参占位符

C++17标准对auto关键字又一次进行了扩展,使它可以作为非类型模板形参的占位符。当然,我们必须保证推导出来的类型是可以用作模板形参的,否则无法通过编译,例如:

#include <iostream> 

template<auto N> 
void f() 
{ 
    cout << N << endl;
}
int main(int argc, char** argv)
{
    f<5>();     // N为int类型  
    f<'c'>();   // N为char类型 
    f<5.0>();   // 编译失败,模板参数不能为double 
    return 0;
}

在上面的代码中,函数f<5>()中5的类型为int,所以auto被推导为int类型。同理,f<'c'>()的auto被推导为char类型。由于 f<5.0>()的5.0被推导为double类型,但是模板参数不能为 double类型,因此导致编译失败。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

神奇的小强

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

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

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

打赏作者

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

抵扣说明:

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

余额充值