第二章 auto

五 优先选用auto,而非显示型别声明

C++11中的auto可以用来声明变量,其型别都是推导自其初始化产物,所以必须进行初始化,这样可以保证合法初始化。

int x;  //未进行初始化,编译没有问题,但后续可能有逻辑问题
auto x; //未进行初始化,无法通过编译

其次可以去掉提领迭代器来声明局部变量时写的超复杂代码:

//C++0x
template<typename T>
void fun(T b, T e)
{
    while(b != e){
        typename itrator_traits<T>::value_type cur = *b;
        ...
    }
}
//C++11
template<typename T>
void fun(T b, T e)
{
    while(b != e){
        auto cur = *b;  
        ...
    }
}

最后由于auto使用了型别推导,就可以使用它来表示只有编译器才知道的型别:

class Widget { };
//C++11
function<bool(const unique_ptr<Widget> &, const unique_ptr<Widget> &)> 
    f1 = [](const unique_ptr<Widget> &p1, const unique_ptr<Widget> &p2) { return p1 < p2; };
auto f2 = [](const unique_ptr<Widget> &p1, const unique_ptr<Widget> &p2) { return p1 < p2; };
//C++14(泛型lambda)
auto f3 = [](const auto &p1, const auto &p2) { return *p1 < *p2; };//可以应用到任何类似指针之物所指向的对象

同时使用auto可以防止我们不小心写错了类型而造成的不必要的隐式类型转换。同时在后期开发的时候,会减少类型改变了之后大规模的改变代码,因为auto可以自行推导类型。

std::function是C++11标准库中的一个模板,它把函数指针的思想加以推广,函数指针只能代指函数,但是std::function可以代指任何可以调用的对象,即任何可以像函数一样调用之物,创建std::function必须要指定涉及到的函数的型别(即参数是什么,返回值是什么)。std::function最重要的概念莫过于闭包和lambda表达式了。

接下来顺带介绍C++ 的闭包lambda表达式。

C++闭包

闭包就是有状态的函数,一个函数,加上了一个状态,就变成了闭包了,带上状态的意思就是这个闭包有属于自己的变量,这个变量的值是创建闭包的时候设置的,并在调用闭包的时候,可以访问这些变量。

函数是代码,状态是一组变量,将代码和一组变量捆绑,就形成了闭包。

闭包的状态捆绑,必须发生在运行时。

对于闭包来说,使用autostd::function来声明是不一样的,使用auto声明,存储着一个闭包的变量和该闭包是一个类型的,故要求的内存量也和该闭包相同;使用std::function声明,存储着闭包的变量是std::function的一个实例,所以不管给定的类型是什么样的,它都占用一块固定的内存,这个内存的大小固定,当内存不够用的时候,会从堆上分配内存。这样std::function声明的闭包会稍微慢于auto声明的闭包。

闭包的实现有三种方式:

  1. 重载operator()
    因为闭包是一个函数+一个状态,这个状态是通过隐含的this指针传入,所以闭包必然是一个函数对象,因为成员变量就是极好的用于保存状态的工具,因此实现operator()运算符重载,该类的作用就能作为闭包使用,默认传入的this指针提供了访问成员变量的途径(事实上,lambda和bind的原理都是这个)
  2. lambda
    lambda是语法糖,可以通过方括号传入同一作用域的变量(值传递[=]或者引用传递[&])
    float round = 0.5;
    auto f = [=](float f){ return f + round; };

  3. std::bind
    bind也是语法糖,是最常用的函数适配器,它可以将函数对象的参数绑定到特定的值,对于没有绑定的参数可以用std::placeholder_X,X从1到20,可以将需要写很多代码的闭包,浓缩到一行bind就可以()直接代替了之前的bind1st和bind2nd。

int main()
{
        vector<int> a{ 1,2,3,4,5,6,7,3,8,3,9 };
        //placeholders::_1代表传入的第一个参数,1代表绑定了minus<int>()的第二个参数
        transform(a.begin(), a.end(), a.begin(), bind(minus<int>(), placeholders::_1, 1));
        for (auto it : a) cout << it << ' ';    //0 1 2 3 4 5 6 2 7 2 8
        cout << endl;
        auto minus10 = bind(minus<int>(), placeholders::_1, 10);
        cout << minus10(30) << endl; //20

        return 0;
}

当bind和function配合使用的时候,就可以将所有的可调用对象的操作方法统一。绑定方法如下:

class A
{
public:
    A(int _a = 2) :a{ _a } { }
    void f(int x){  //成员函数
        cout << a << ' ' << x << endl;
    }
    private:
        int a;
};
struct comp {   //函数对象
    bool operator()(const int p1, const int p2) { return p1 < p2; }
};
bool compf(const int p1, const int p2){ return p1 < p2; }   //函数指针
int main()
{
    vector<int> a{ 1,2,3,4,5,6,7,3,8,3,9 };
    vector<int> b(11);
    A aa;
    //绑定成员函数
    auto testf = bind(&A::f, &aa, placeholders::_1);
    testf(3);   //2 3
    //绑定lambda
    auto f2 = [](const int p1, const int p2) { return p1 < p2; };
    auto testf2 = bind(f2, placeholders::_1, 3);
    transform(a.begin(), a.end(), b.begin(), testf2);
    for (auto it : b) cout << it << ' ';    //1 1 0 0 0 0 0 0 0 0 0
    cout << endl;
    //绑定函数对象
    auto testf3 = bind(&comp::operator(), comp(), placeholders::_1, 3);
    transform(a.begin(), a.end(), b.begin(), testf3);
    for (auto it : b) cout << it << ' ';    //1 1 0 0 0 0 0 0 0 0 0
    cout << endl;
    //绑定函数指针
    auto testf4 = bind(compf, placeholders::_1, 3);
    transform(a.begin(), a.end(), b.begin(), testf3);
    for (auto it : b) cout << it << ' ';    //1 1 0 0 0 0 0 0 0 0 0
    cout << endl;

    return 0;
}

lambda表达式

参考博客:C++11 lambda表达式与函数对象

lambda表达式的原理是每当定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),称之为闭包类型,那么在运行时,这个lambda表达式的结果就是一个闭包,闭包的强大之处是可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式和变量,称其lambda捕捉块。捕捉块可以为空,表示不传入任何变量。

lambda表达式的完整语法如下:

//其中mutable、constexpr、exception、attribute和返回类型都是可选的,attribute和lambda表达式特性有关
[捕捉块](传入参数) mutable constexpr(c++17) exception attribute -> 返回类型 {函数体}
//简化版本:
[捕捉块](传入参数)->ret{函数体}
[捕捉块](传入参数){函数体}
[捕捉块]{函数体}

事实上,通过捕捉块传入的值是在类内部形成了非静态成员变量,函数调用运算符的重载方法是const属性的,但是如果想要改变按值传入的变量的值,就需要增加mutable属性。

int main
{
    int x = 10;
    auto add_x = [x](int a) mutable { x *= 2; return a + x; };  //没有mutable就不能改变x的值(改变不会改变原来的x的值,只会改变传入的x的值)
    cout << add_x(10) << endl;

    return 0;
}

对于引用传入的方式,无论是否为mutable属性,都可以在lambda表达式中修改捕获的值,但是闭包类中是否有对应成员和具体实现有关。

lambda表达式是不能被赋值的。即相同类型的lambda表达式不能进行”=”赋值。因为闭包类中的赋值符被禁止了。

捕获方式如下:

  • []:不捕获任何变量
  • [=]:按值捕获所有变量
  • [&]:按引用捕获所有变量
  • [x]:只捕获变量x
  • [&x]:只以引用方式捕获变量x
  • [=, &x]:按值捕获所有变量,以引用方式捕获变量x
  • [&, x]:按引用捕获所有变量,按值捕获变量x
  • [this]:通过引用捕获当前对象,其实是复制指针
  • [*this]:通过传值方式捕获当前对象

上面的捕获方式中,最好不要使用[=]和[&]默认捕获所有变量。会产生悬挂引用,因为按引用捕获不会延长引用变量的声明周期。

例如我们有一个可以返回lambda表达式的函数f,函数内部返回的lambda表达式按引用捕获了函数f内部的临时变量,并用这个临时变量参与了lambda表达式的计算,这样计算出来的结果很有可能是有问题的,因为返回了lambda表达式并调用这个表达式的时候,刚才以引用捕获的变量已经超出了作用域,这个时候变量的值是未定义的。

function<int(int)> add_x(int x)
{
    return [&](int a) { return x + a; };
}
int main()
{
    auto f = add_x(30);
    int ret = f(20);
    cout << ret << endl;    //第一次计算的结果可能正确(50),但是更改30的值,计算出的结果可能就会出问题

    return 0;
}

但是按值传递呢?其实也会有问题,假使有一个类有返回lambda表达式的成员函数,按值传递类内变量的话,其实是按照传入this的方式来读取内部的变量(私有或者公有)。这个时候如果类被析构了,而再使用lambda表达式的话,还是会出现问题。例子如下:

class A
{
public:
    A(int _a) :a{ _a } { }
    decltype(auto) func() {
        return [=](int v) { return v % a == 0; };
    }
    ~A() { a = 0; }
private:
    int a;
};
int main()
{
    A a(3);
    auto f = a.func();
    a.~A();
    bool ret = f(3);    //加上这一句代码直接崩了
    cout << ret << endl;

    return 0;
}

上述代码的lambda表达式很明显在调用的时候和类内成员a并不是一个作用域,但是为什么可以访问呢,其实是因为lambda表达式的真身是[this](int v) { return v % this->a == 0; };,也就是说,传入的是this指针。也就是说这个lambda表达式其实是和类对象产生了关系,所以当对象被析构之后,代码就陷入了未定义的困境。其实真正的原因是类内的按值传递,其实是指针变量的复制和传递。

lambda表达式可以代替函数对象在STL众多算法中的位置,也就是说本来我们需要用函数对象传入的地方,现在都可以直接写入lambda表达式。

int main()
{
    int val = 3;
    vector<int> a{1,2,3,4,5,6,7,3,8,3,9};
    int cnt = count_if(a.begin(), a.end(), [val](int x) { return x >= val; });
    cout << cnt << endl;    //9

    return 0;
}

或者是斐波那契数列:

vector<int> v(10);
int aa = 0;
int bb = 1;
generate(v.begin(), v.end(), [&aa, &bb]() { int value = bb; bb = aa + bb; aa = value; return value; });
for (auto it : v) cout << it << ' ';

C++14中lambda表达式支持捕捉表达式和泛型,泛型在之前已经说过,就是参数可以以auto声明,auto推断类型的规则和函数模板一样,不用声明具体类型,就类似于函数模板。

auto f3 = [](const auto &p1, const auto &p2) { return *p1 < *p2; };

int main()
{
    int arr[] = { 1,2,3,4,5 };
    vector<int> vec(arr, arr + 5);
    int *p1 = arr;
    int *p2 = arr + 3;
    cout << f3(p1, p2) << endl;
    cout << f3(vec.begin(), vec.begin() + 3) << endl;
    return 0;
}

对于捕捉表达式就是可以捕获不在同一作用域内的变量,也可以捕获右值(通过std::move())。

int main()
{
    int x = 4;
    auto fun = [&r = x, x = x + 1]{ r += 2; return x * x; };
    auto fun2 = [str = "string"]{ return str; };
    cout << fun() << endl;//25
    cout << x << endl;  //6
    cout << fun2() << endl;//string
    auto pi = make_unique<double>(3.1415);  //make_unique只能移动不能复制,所以只能用右值的方式进行移动
    auto area = [pi = move(pi)](double r){ return *pi * r * r; };
    //auto area = [pi = forward<unique_ptr<double>&&>(pi)](double r){ return *pi * r * r; }; //当然也可以用完美转发,传入什么类型就转换成什么类型
    cout << area(2.0) << endl;//12.566

    return 0;
}

六 当auto推导的型别不符合要求时,使用带显式型别的初始化物习惯用法

代理类

首先开始本章内容之前,先介绍一下代理类。

C++在定义容器存储类型的时候,通常要面对的是一个派生系列,这样的话,要面临的问题有:

  1. 多态的问题
  2. 内存空间申请和释放的问题
  3. 静态类型的问题

1问题很好解决,只要我们不直接在容器中存储对象,而在容器中存储指针或者引用就可以运用到多态这个性质。

2问题由1问题带来,释放内存后如果再对对应的内存进行处理的话,就会出现未定义行为。

3问题,如果我们想依据容器中的某一个对象来进行另一个对象的构造的话,我们很难去知道类型究竟是继承链中的哪一个。

所以我们需要一个代理类,代理类所代理的继承链表中的父类是虚基类,必定有一个纯虚函数copy(),这个纯虚函数就是为了通过一个类去构造另一个类,每次都通过多态的方式,返回本类型new的对象。代理类中,维护一个基类的指针(当然最好是使用智能指针),这样代理类再构造的时候直接调用维护的类指针的copy()函数就可以,这样一是可以避免显式的内存分配,二是保持类类型再运行期的绑定。

代理类的每个对象都代表另一个对象,该对象可以是位于一个完整继承层次中的任何类的对象,通过再容器中使用代理对象而不是对象本身的方式,就是代理类的本质。

vector< bool>

接下来介绍一下vector<bool>,当vector中存储的变量是bool时,不是模板的流程,而是一个特化版本,这个特化版本用每一个bit来存储每一个bool,可以减少存储空间,但是C++不允许bit的引用,所以operator[]不能返回T&。也就是说正常情况下vector<T>::reference(嵌套在vector<bool>中的类)的类型是T,但是唯独vector<bool>::reference的类型不是bool。但是这个类里面重载运算符operator()做了一个向bool的隐式转换。

int main()
{
    vector<bool> v{ true, false };
    vector<int> v2{1, 0};
    cout << typeid(v[0]).name() << endl;//class std::_Vb_reference<struct std::_Wrap_alloc<class std::allocator<unsigned int> > >
    cout << typeid(v2[0]).name() << endl;//int

    return 0;
}

由上述代码可知,这里的类型其实比较混乱,vector<bool>::reference的类型对象指向一个机器字,该机器字持有被引用的bit,再通过偏移就能知道对应的bool值。所以当我们执行

bool tmp = v[1];

的时候,这里先是返回了_Vb_reference类型,然后隐式转换为bool类型。这个_Vb_reference类就是一个代理类。

但是当我们用auto来推导返回类型时,auto是不会执行隐式转换的,这就导致我们推导出来的类型其实是代理类的类型。但是如果一个函数的返回类型是vector<bool>,那么当使用如下代码:

#include <iostream>
#include <vector>
using namespace std;
vector<bool> test()
{
    vector<bool> v{ true, false, true, false, true, false };
    return v;
}
int main()
{

    auto a = test()[5];
    cout << a << endl;  //代码崩溃

    return 0;
}

很明显test()返回了一个临时对象,假设其为temp,并且紧接着针对temp执行operator[],返回一个_Vb_reference类型欸但对象,该对象含有一个指针,指向temp所管理的一个数据结构(用bit存bool值),当temp随着作用域结束而被析构之后,a就是一个野指针了。这样对a指向解引用,一定会出问题。

所以很明显,隐式的代理类和auto产生了冲突,所以解决方法之一可以使用强制类型转换:

auto a = static_cast<bool>(test()[5]);

猜你喜欢

转载自blog.csdn.net/u012630961/article/details/80643641