zoukankan      html  css  js  c++  java
  • C++11——自动类型推导

    转载来自:https://subingwen.cn/cpp/autotype/

    在 C++11 中增加了很多新的特性,比如可以使用 auto 自动推导变量的类型,还能够结合 decltype 来表示函数的返回值。使用新的特性可以让我们写出更加简洁,更加现代的代码。

    1. auto
    在 C++11 之前 auto 和 static 是对应的,表示变量是自动存储的,但是非 static 的局部变量默认都是自动存储的,因此这个关键字变得非常鸡肋,在 C++11 中他们赋予了新的含义,使用这个关键字能够像别的语言一样自动推导出变量的实际类型。

    1.1 推导规则
    C++11 中 auto 并不代表一种实际的数据类型,只是一个类型声明的 “占位符”,auto 并不是万能的在任意场景下都能够推导出变量的实际类型,使用auto声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型。使用语法如下:

    auto 变量名 = 变量值;
    auto x = 3.14;      // x 是浮点型 double
    auto y = 520;       // y 是整形 int
    auto z = 'a';       // z 是字符型 char
    auto nb;            // error,变量必须要初始化
    auto double nbl;    // 语法错误, 不能修改数据类型   

    不仅如此,auto 还可以和指针、引用结合起来使用也可以带上 const、volatile 限定符,在不同的场景下有对应的推导规则,规则内容如下:

    当变量不是指针或者引用类型时,推导的结果中不会保留 const、volatile 关键字
    当变量是指针或者引用类型时,推导的结果中会保留 const、volatile 关键字

    int temp = 110;
    auto *a = &temp;    
    auto b = &temp;        
    auto &c = temp;        
    auto d = temp;    

     这边可以看到

    a 是 int*

    b 是int*

    c 是int&

    d 是int

        int tmp = 250;
        const auto a1 = tmp;
        auto a2 = a1;
        const auto& a3 = tmp;
        auto& a4 = a3;

     可以看到

    a1 是 const int

    a2却看到const没有了

    a3是const int&

    a4是const int&

    1.2 auto的限制

    auto 关键字并不是万能的,在以下这些场景中是不能完成类型推导的

    不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto 要求必须要给修饰的变量赋值,因此二者矛盾。

    int func(auto a, auto b) // error
    {
        cout << "a: " << a << ", b: " << b << endl;
    }

    不能用于类的非静态成员变量的初始化

    class Test
    {
        auto v1 = 0; // error
        static auto v2 = 0; // error,类的静态非常量成员不允许在类内部直接初始化
        static const auto v3 = 10; // ok
    }

    不能使用 auto 关键字定义数组

            int array[] = { 1,2,3,4,5 }; // 定义数组
            auto t1 = array; // ok, t1被推导为 int* 类型
            auto t2[] = array; // error, auto无法定义数组
            auto t3[] = { 1,2,3,4,5 }; // error, auto无法定义数组

    无法使用 auto 推导出模板参数

    template <typename T>
        struct Test {}
    
        int func()
        {
            Test<double> t;
            Test<auto> t1 = t; // error, 无法推导出模板类型
            return 0;
        }

    1.3 auto 的应用

    了解了 auto 的限制之后,我们就可以避开这些场景快乐的编程了,下面列举几个比较常用的场景:

    用于STL的容器遍历。

    在 C++11 之前,定义了一个 stl 容器之后,遍历的时候常常会写出这样的代码:

    #include <map>
    int main()
    {
        map<int, string> person;
        map<int, string>::iterator it = person.begin();
        for (; it != person.end(); ++it)
        {
            // do something
        }
        return 0;
    }

    可以看到在定义迭代器变量 it 的时候代码是很长的,写起来就很麻烦,使用了 auto 之后,就变得清爽了不少:

    #include <map>
    int main()
    {
        map<int, string> person;
        // 代码简化
        for (auto it = person.begin(); it != person.end(); ++it)
        {
            // do something
        }
        return 0;
    }

    用于泛型编程,在使用模板的时候,很多情况下我们不知道变量应该定义为什么类型,比如下面的代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class T1
    {
    public:
        static int get()
        {
            return 10;
        }
    };
    
    class T2
    {
    public:
        static string get()
        {
            return "hello, world";
        }
    };
    
    template <class A>
    void func(void)
    {
        auto val = A::get();
        cout << "val: " << val << endl;
    }
    
    int main()
    {
        func<T1>();
        func<T2>();
        return 0;
    }

    在这个例子中定义了泛型函数 func,在函数中调用了类 A 的静态方法 get () ,这个函数的返回值是不能确定的如果不使用 auto,就需要再定义一个模板参数,并且在外部调用时手动指定 get 的返回值类型,具体代码如下:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class T1
    {
    public:
        static int get()
        {
            return 0;
        }
    };
    
    class T2
    {
    public:
        static string get()
        {
            return "hello, world";
        }
    };
    
    template <class A, typename B> // 添加了模板参数 B
    void func(void)
    {
        B val = A::get();
        cout << "val: " << val << endl;
    }
    
    int main()
    {
        func<T1, int>(); // 手动指定返回值类型 -> int
        func<T2, string>(); // 手动指定返回值类型 -> string
        return 0;
    }

    2. decltype
    在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用 C++11 提供的 decltype 关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型,语法格式如下:

    decltype (表达式)
    decltype 是 “declare type” 的缩写,意思是 “声明类型”。decltype 的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。来看一组简单的例子:

    int a = 10;
    decltype(a) b = 99; // b -> int
    decltype(a+3.14) c = 52.13; // c -> double
    decltype(a+b*c) d = 520.1314; // d -> double

    可以看到 decltype 推导的表达式可简单可复杂,在这一点上 auto 是做不到的,auto 只能推导已初始化的变量类型

    2.1 推导规则
    通过上面的例子我们初步感受了一下 decltype 的用法,但不要认为 decltype 就这么简单,在它简单的背后隐藏着很多的细节,下面分三个场景依次讨论一下:

    表达式为普通变量或者普通表达式或者类表达式,在这种情况下,使用 decltype 推导出的类型和表达式的类型是一致的。

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Test
    {
    public:
        string text;
        static const int value = 110;
    };
    
    int main()
    {
        int x = 99;
        const int& y = x;
        decltype(x) a = x;
        decltype(y) b = x;
        decltype(Test::value) c = 0;
    
        Test t;
        decltype(t.text) d = "hello, world";
    
        return 0;
    }

    变量 a 被推导为 int 类型
    变量 b 被推导为 const int & 类型
    变量 c 被推导为 const int 类型
    变量 d 被推导为 string 类型
    表达式是函数调用,使用 decltype 推导出的类型和函数返回值一致。

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Test
    {
    public:
        string text;
        static const int value = 110;
    };
    //函数声明
    int func_int(); // 返回值为 int
    int& func_int_r(); // 返回值为 int&
    int&& func_int_rr(); // 返回值为 int&&
    const int func_cint(); // 返回值为 const int
    const int& func_cint_r(); // 返回值为 const int&
    const int&& func_cint_rr(); // 返回值为 const int&&
    const Test func_ctest(); // 返回值为 const Test
    
    int main()
    {
        //decltype类型推导
        int n = 100;
        decltype(func_int()) a = 0;
        decltype(func_int_r()) b = n;
        decltype(func_int_rr()) c = 0;
        decltype(func_cint()) d = 0;
        decltype(func_cint_r()) e = n;
        decltype(func_cint_rr()) f = 0;
        decltype(func_ctest()) g = Test();
    
        return 0;
    }

     变量 a 被推导为 int 类型

    变量 b 被推导为 int& 类型
    变量 c 被推导为 int&& 类型
    变量 d 被推导为 int 类型
    变量 e 被推导为 const int & 类型
    变量 f 被推导为 const int && 类型
    变量 g 被推导为 const Test 类型

    函数 func_cint () 返回的是一个纯右值(在表达式执行结束后不再存在的数据,也就是临时性的数据)

    对于纯右值而言,只有类类型可以携带const、volatile限定符除此之外需要忽略掉这两个限定符,因此推导出的变量 d 的类型为 int 而不是 const int。

    表达式是一个左值,或者被括号 ( ) 包围使用 decltype 推导出的是表达式类型的引用(如果有 const、volatile 限定符不能忽略)。

    #include <iostream>
    #include <vector>
    using namespace std;
    
    class Test
    {
    public:
        int num;
    };
    
    int main() {
        const Test obj;
        //带有括号的表达式
        decltype(obj.num) a = 0;
        decltype((obj.num)) b = a;
        //加法表达式
        int n = 0, m = 0;
        decltype(n + m) c = 0;
        decltype(n = n + m) d = n;
        return 0;
    }

    obj.num 为类的成员访问表达式,符合场景 1,因此 a 的类型为 int
    obj.num 带有括号,符合场景 3,因此 b 的类型为 const int&。
    n+m 得到一个右值,符合场景 1,因此 c 的类型为 int
    n=n+m 得到一个左值 n,符合场景 3,因此 d 的类型为 int&

    2.2 decltype 的应用
    关于 decltype 的应用多出现在泛型编程中。比如我们编写一个类模板,在里边添加遍历容器的函数,操作如下:

    #include <list>
    using namespace std;
    
    template <class T>
    class Container
    {
    public:
        void func(T& c)
        {
            for (m_it = c.begin(); m_it != c.end(); ++m_it)
            {
                cout << *m_it << " ";
            }
            cout << endl;
        }
    private:
        ? ? ? m_it; // 这里不能确定迭代器类型
    };
    
    int main()
    {
        const list<int> lst;
        Container<const list<int>> obj;
        obj.func(lst);
        return 0;
    }

    在程序的第 17 行出了问题,关于迭代器变量一共有两种类型:只读(T::const_iterator)和读写(T::iterator),有了 decltype 就可以完美的解决这个问题了当 T 是一个 非 const 容器得到一个 T::iterator,当 T 是一个 const 容器时就会得到一个 T::const_iterator。

    #include <list>
    #include <iostream>
    using namespace std;
    
    template <class T>
    class Container
    {
    public:
        void func(T& c)
        {
            for (m_it = c.begin(); m_it != c.end(); ++m_it)
            {
                cout << *m_it << " ";
            }
            cout << endl;
        }
    private:
        decltype(T().begin()) m_it; // 这里不能确定迭代器类型
    };
    
    int main()
    {
        const list<int> lst{ 1,2,3,4,5,6,7,8,9 };
        Container<const list<int>> obj;
        obj.func(lst);
        return 0;
    }#include <list>
    #include <iostream>
    using namespace std;
    
    template <class T>
    class Container
    {
    public:
        void func(T& c)
        {
            for (m_it = c.begin(); m_it != c.end(); ++m_it)
            {
                cout << *m_it << " ";
            }
            cout << endl;
        }
    private:
        decltype(T().begin()) m_it; // 这里不能确定迭代器类型
    };
    
    int main()
    {
        const list<int> lst{ 1,2,3,4,5,6,7,8,9 };
        Container<const list<int>> obj;
        obj.func(lst);
        return 0;
    }

    decltype(T().begin()) 这种写法在 vs2017/vs2019 下测试可用完美运行。

    3. 返回类型后置
    在泛型编程中,可能需要通过参数的运算来得到返回值的类型,比如下面这个场景:

    #include <iostream>
    using namespace std;
    // R->返回值类型, T->参数1类型, U->参数2类型
    template <typename R, typename T, typename U>
    R add(T t, U u)
    {
        return t + u;
    }
    
    int main()
    {
        int x = 520;
        double y = 13.14;
        // auto z = add<decltype(x + y), int, double>(x, y);
        auto z = add<decltype(x + y)>(x, y); // 简化之后的写法
        cout << "z: " << z << endl;
        return 0;
    }

    关于返回值,从上面的代码可以推断出和表达式 t+u 的结果类型是一样的,因此可以通过通过 decltype 进行推导,关于模板函数的参数 t 和 u 可以通过实参自动推导出来,因此在程序中就也可以不写。虽然通过上述方式问题被解决了,但是解决方案有点过于理想化,因为对于调用者来说,是不知道函数内部执行了什么样的处理动作的。

    因此如果要想解决这个问题就得直接在 add 函数身上做文章,先来看第一种写法:

    template <typename T, typename U>
    decltype(t + u) add(T t, U u)
    {
        return t + u;
    }

    当我们在编译器中将这几行代码改出来后就直接报错了因此 decltype 中的 t 和 u 都是函数参数,直接这样写相当于变量还没有定义就直接用上了,这时候变量还不存在,有点心急了。

    在C++11中增加了返回类型后置语法,说明白一点就是将decltype和auto结合起来完成返回类型的推导。其语法格式如下:

    // 符号 -> 后边跟随的是函数返回值的类型
    auto func(参数1, 参数2, ...) -> decltype(参数表达式)

    通过对上述返回类型后置语法代码的分析,得到结论:auto 会追踪 decltype() 推导出的类型因此上边的 add() 函数可以做如下的修改:

    #include <iostream>
    using namespace std;
    
    template <typename T, typename U>
    // 返回类型后置语法
    auto add(T t, U u) -> decltype(t + u)
    {
        return t + u;
    }
    
    int main()
    {
        int x = 520;
        double y = 13.14;
        // auto z = add<int, double>(x, y);
        auto z = add(x, y); // 简化之后的写法
        cout << "z: " << z << endl;
        return 0;
    }

    为了进一步说明这个语法,我们再看一个例子:

    #include <iostream>
    using namespace std;
    
    int& test(int& i)
    {
        return i;
    }
    
    double test(double& d)
    {
        d = d + 100;
        return d;
    }
    
    template <typename T>
    // 返回类型后置语法
    auto myFunc(T& t) -> decltype(test(t))
    {
        return test(t);
    }
    
    int main()
    {
        int x = 520;
        double y = 13.14;
        // auto z = myFunc<int>(x);
        auto z = myFunc(x); // 简化之后的写法
        cout << "z: " << z << endl;
    
        // auto z = myFunc<double>(y);
        auto z1 = myFunc(y); // 简化之后的写法
        cout << "z1: " << z1 << endl;
        return 0;
    }

    在这个例子中,通过 decltype 结合返回值后置语法很容易推导出来 test(t) 函数可能出现的返回值类型,并将其作用到了函数 myFunc() 上。

  • 相关阅读:
    【python】opencv教程CV2模块——翻转图片并保存
    【python】opencv教程CV2模块——简单画图
    【python】opencv教程CV2模块——复制图片、灰度模式并保存
    【python】opencv教程CV2模块——窗口显示图片、按键盘指定s键保存
    【python】opencv教程CV2模块——打开读取图片、窗口显示、保存图片
    【前端】一行代码实现网站网页变黑白灰效果
    【python】彩色图秒变酷炫黑白图,灰度模式,比PS还好用的图像处理,cv2
    【python】批量调整图片亮度和饱和度,比PS还方便的图片处理,cv2
    【python】使用滑动条调整图片亮度和饱和度,比PS还方便的图片处理,cv2
    代码-JS之正则验证手机格式
  • 原文地址:https://www.cnblogs.com/Galesaur-wcy/p/15307203.html
Copyright © 2011-2022 走看看