美文网首页Effective Modern C++
【Effective Modern C++(1)】类型推断

【Effective Modern C++(1)】类型推断

作者: downdemo | 来源:发表于2018-11-23 10:16 被阅读35次

    01 理解模板类型推断

    • 模板类型推断是auto的基础,但部分特殊情况下模板推断的机制不适用于auto
    • 模板的形式可以看成如下伪代码
    template<typename T>
    void f(paramType param);
    
    • 调用可以看成
    f(expr);
    
    • 编译期间,编译器用expr推断两个类型:T的类型和ParamType的类型。这些类型通常不同,因为ParamType通常包含限定符,比如
    template<typename T>
    void f(const T& param); // ParamType是const T&
    
    int x = 0;
    f(x); // T被推断为int,ParamType被推断为const int&
    
    • T的类型推断不仅依赖于expr,也依赖于ParamType, 有三种情况
      • ParamType是指针或引用,但不是转发引用
      • ParamType是转发引用
      • ParamType既不是指针也不是引用

    情形1:ParamType是指针/引用类型

    • 如果expr的类型是引用,忽略引用再用匹配expr的ParamType类型来确定T
    template<typename T>
    void f(T& param);
    
    int x = 27;
    const int cx = x;
    const int& rx = x;
    
    f(x); // T是int,ParamType是int&
    f(cx); // T是const int,ParamType是const int&
    f(rx); // T是const int,ParamType是const int&
    
    • 如果把f的参数类型从T&改为const T&,如果expr是引用类型,引用部分仍会被忽略,但有一些微小的不同,T不用再推断为const
    template<typename T>
    void f(const T& param);
    
    int x = 27;
    const int cx = x;
    const int& rx = x;
    
    f(x); // T是int,ParamType是const int&
    f(cx); // T是int,ParamType是const int&
    f(rx); // T是int,ParamType是const int&
    
    • 如果param是指针(或pointer to const),情况一致
    template<typename T>
    void f(const T* param);
    
    int x = 27;
    const int* px = &x;
    
    f(&x); // T是int,ParamType是int*
    f(px); // T是const int,ParamType是const int*
    

    情形2:ParamType是转发引用

    • 如果expr是左值,T和ParamType都推断为左值引用。这有两点非常特殊
      • 这是T被推断为引用的唯一情形
      • ParamType使用右值引用语法,却被推断为左值引用
    • 如果expr是右值,适用情形1的正常规则
    template<typename T>
    void f(T&& param);
    
    int x = 27;
    const int cx = x;
    const int& rx = x;
    int&& rr = 27; // rr是右值引用,但也是左值
    
    f(x); // T是int&,ParamType是int&
    f(cx); // T是const int&,ParamType是const int&
    f(rx); // T是const int&,ParamType是const int&
    f(rx); // T是const int&,ParamType是const int&
    f(27); // T是int,ParamType是int&&
    

    情形3:ParamType不是引用或指针

    • 如果expr是引用,忽略引用部分,如果还有cv限定符也忽略,这也是唯一忽略cv限定符的情形
    template<typename T>
    void f(T param);
    
    int x = 27;
    const int cx = x;
    const int& rx = x;
    
    f(x); // T和ParamType都是int
    f(cx); // T和ParamType都是int
    f(rx); // T和ParamType都是int
    
    • 如果expr是指针,则保留cv限定符
    template<typename T>
    void f(T param);
    
    const char* const ptr = "Fun with pointers";
    const char name[] = "downdemo";
    f(ptr); // T和ParamType都是const char* const
    f(name); // T和ParamType都是const char*
    

    特殊情形1:expr是数组名

    template<typename T> void f1(T param);
    template<typename T> void f2(T& param);
    template<typename T> void f3(T&& param);
    
    const char name[9] = "downdemo";
    f1(name); // T和ParamType都是const char*
    f2(name); // T是const char[9],ParamType是const char(&)[9],即const char(&param)[9]
    f3(name); // 同上,T是const char[9],ParamType是const char(&)[9]
    
    • 声明ParamType为数组的引用(T(&)[N])能用来创建推断数组元素数量的模板
    template <typename T, std::size_t N>
    constexpr std::size_t arraySize(T (&)[N]) noexcept
    {
        return N;
    }
    const char name[] = "downdemo";
    int a[arraySize(name)]; // int a[9]
    

    特殊情形2:expr是函数名

    void someFunc(int, double);
    template<typename T> void f1(T param);
    template<typename T> void f2(T& param);
    template<typename T> void f3(T&& param);
    
    f1(someFunc); // 推断为ptr-to-fun,T和ParamType都是void(*)(int, double)
    f2(someFunc); // 推断为ref-to-fun,T和ParamType都是void(&)(int, double)
    f3(someFunc); // 同上,T和ParamType都是void(&)(int, double)
    

    02 理解auto类型推断

    • auto类型推断几乎和模板类型推断一致
    • 调用模板时,编译器根据expr推断T和ParamType的类型。当变量用auto声明时,auto就扮演了模板中的T的角色,变量的类型修饰符则扮演ParamType的角色
    • 为了推断变量类型,编译器表现得好比每个声明对应一个模板,模板的调用就相当于对应的初始化表达式
    auto x = 27;
    const auto cx = x;
    const auto& rx = x;
    
    template<typename T> // 用来推断x类型的概念上假想的模板
    void func_for_x(T param);
    func_for_x(27); // 假想的调用: param的推断类型就是x的类型
    
    template<typename T> // 用来推断cx类型的概念上假想的模板
    void func_for_cx(const T param);
    func_for_cx(x); // 假想的调用: param的推断类型就是cx的类型
    
    template<typename T> // 用来推断rx类型的概念上假想的模板
    void func_for_rx(const T& param);
    func_for_rx(x); // 假想的调用: param的推断类型就是rx的类型
    
    • auto的推断适用模板推断机制的三种情形:T&、T&&和T
    auto x = 27; // int x
    const auto cx = x; // const int cx
    const auto& rx = x; // const int& rx
    auto&& uref1 = x; // int& uref1
    auto&& uref2 = cx; // const int& uref2
    auto&& uref3 = 27; // int&& uref3
    
    • auto对数组和指针的推断也和模板一致
    const char name[] = "downdemo"; // 数组类型是const char[9]
    auto arr1 = name; // const char* arr1
    auto& arr2 = name; // const char (&arr2)[9]
    
    void someFunc(int, double); // 函数类型是void(int, double)
    auto func1 = someFunc; // void (*func1)(int, double)
    auto& func2 = someFunc; // void (&func2)(int, double)
    
    • auto推断唯一不同于模板推断的情形是C++11的初始化列表,下面实现的都是同样的赋值功能
    // C++98
    int x1 = 27;
    int x2(27);
    // C++11
    int x3 = { 27 };
    int x4{ 27 };
    
    • 但对于auto,这些赋值有不同的意义
    auto x1 = 27; // int x1
    auto x2(27); // int x2
    auto x3 = { 27 }; // std::initializer_list<int> x3
    auto x4{ 27 }; // C++11为std::initializer_list<int> x4,C++14为int x4
    
    • 如果初始化列表中元素类型不同,则无法推断
    auto x5 = { 1, 2, 3.0 }; // 错误:不能为std::initializer_list<T>推断T
    
    • C++14中禁止对auto用std::initializer_list直接初始化,而必须用=,除非列表中只有一个元素,这时不会将其视为initializer_list
    auto x1 = { 1, 2 }; // C++14中必须用=
    auto x2 { 1 }; // 保留了单元素列表的直接初始化,但不会将其视为initializer_list
    
    • 而模板不支持ParamType为T,expr为初始化列表的推断,不会将其假设为std::initializer_list,这就是auto推断和模板推断唯一的不同之处
    auto x = { 11, 23, 9 }; // x类型是std::initializer_list<int>
    
    template<typename T> // 等价于x声明的模板
    void f(T param);
    
    f({ 11, 23, 9 }); // 错误:不能推断T的类型
    
    • 不过对模板指定ParamType为std::initializer_list<T>则可以推断T
    template<typename T>
    void f(std::initializer_list<T> initList);
    
    f({ 11, 23, 9 }); // T被推断为int,initList类型是std::initializer_list<int>
    
    • 对于C++11,auto的介绍就到此为止了

    C++14的auto

    • C++14中,auto可以作为函数返回类型,并且lambda可以将参数声明为auto
    auto f() { return 1; }
    auto g = [](auto x) {return x; };
    
    • 但此时auto仍然使用的是模板实参演绎的机制,因此返回类型为auto的函数如果返回一个初始化列表,则会出错
    auto newInitList() { return { 1 }; } // 错误
    
    • 将参数声明为auto的lambda同理
    std::vector<int> v { 2, 4, 6};
    auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
    resetV({ 1, 2, 3 }); // 错误
    

    03 理解decltype

    • 给定一个名称或表达式,decltype会不出所料地推断出预期的确切类型
    const int i = 0; // decltype(i)是const int
    
    bool f(const Widget& w); // decltype(w)是const Widget&,decltype(f)是bool(const Widget&)
    
    struct Point {
        int x, y; // decltype(Point::x)和decltype(Point::y)是int
    };
    
    Widget w; // decltype(w)是Widget
    if (f(w)) … // decltype(f(w))是bool
    
    int a[] {1, 2, 3}; // decltype(a)是int[3]
    
    template<typename T> // std::vector的简化版
    class vector {
    public:
        …
        T& operator[](std::size_t index);
        …
    };
    vector<int> v; // decltype(v)是vector<int>
    …
    if (v[0] == 0) … // decltype(v[0])是int&
    
    • 在C++11中,decltype的主要用法可能是为返回类型依赖于参数类型的函数声明模板,比如写一个使用索引访问容器的函数,返回类型是元素类型的引用,依赖于元素类型T,而使用decltype则能轻松地表达这点
    template<typename Container, typename Index>
    auto authAndAccess(Container& c, Index i) -> decltype(c[i])
    {
        authenticateUser(); // 验证用户有效
        return c[i];
    }
    
    • 上述的auto没有做任何事,只是表示使用类型推断,推断使用的是decltype
    • C++11允许推断single-statement lambda的返回类型,C++14把这点扩展到了所有lambda和函数。对于上例,C++14允许省略尾置返回类型,只留下auto,这样编译器将从函数的实现推断返回类型
    template<typename Container, typename Index>
    auto authAndAccess(Container& c, Index i)
    {
        authenticateUser(); // 验证用户有效
        return c[i];
    }
    
    • 之前提到过,auto作为函数返回类型使用的是模板推断机制,而在这个例子中,这将造成问题。operator[]返回元素引用,类型为T&,但模板的推断会忽略引用,因此下面的用法是错误的
    std::deque<int> d;
    …
    authAndAccess(d, 5) = 10; // 返回d[5]然后赋值为10,但不能通过编译
    
    • d[5]返回int&,auto推断为int,因此返回的实际是一个右值的整型字面值,上述行为相当于把10赋给一个右值,这显然是错误的
    • 为了得到期望的返回类型,需要对返回类型使用decltype的推断机制,C++14中允许将返回类型声明为decltype(auto)来实现这点
    template<typename Container, typename Index>
    decltype(auto)
    authAndAccess(Container& c, Index i)
    {
        authenticateUser();
        return c[i];
    }
    
    • decltype(auto)不仅限于作为函数返回类型,也可以作为变量声明类型
    Widget w;
    const Widget& cw = w;
    auto myWidget1 = cw; // auto类型推断:myWidget1类型是Widget
    decltype(auto) myWidget2 = cw; // decltype类型推断:myWidget2类型是const Widget&
    
    • 但还有一些问题,容器传的是non-const左值引用,这就无法接受右值
    template<typename Container, typename Index>
    decltype(auto) authAndAccess(Container& c, Index i);
    
    • 比如一个右值容器是一个调用authAndAccess后就销毁的临时对象
    std::deque<std::string> makeStringDeque(); // 工厂函数
    // 拷贝从makeStringDeque返回的第五个元素
    auto s = authAndAccess(makeStringDeque(), 5);
    
    • 为了支持这种用法,需要允许接受左值和右值。重载是可行的(一个将参数声明为左值引用,另一个声明为右值引用),但要维护两个函数。避免这点的方法是使用能绑定左值和右值的转发引用作为参数,并使用std::forward转发原有类型
    // 最终的C++14版本,c现在是一个转发引用
    template<typename Container, typename Index>
    decltype(auto) authAndAccess(Container&& c, Index i)
    {
        authenticateUser();
        return std::forward<Container>(c)[i];
    }
    
    • 如果要使用C++11版本,只需要多指定返回类型
    // 最终的C++11版本,将decltype(auto)改为auto结合尾置返回类型
    template<typename Container, typename Index>
    auto
    authAndAccess(Container&& c, Index i) // version
    -> decltype(std::forward<Container>(c)[i])
    {
        authenticateUser();
        return std::forward<Container>(c)[i];
    }
    

    decltype的特殊情况

    • 如果表达式是解引用,decltype会推断为引用类型
    int* p; // decltype(*p)是int&
    
    • 赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型
    int i = 0; // decltype(i=1)是int&
    int n = 4;
    decltype(i=1) r = n;
    r = 5;
    std::cout << i << n << r; // 055
    
    • 如果表达式加上一层或多层括号,编译器会将其看作表达式,变量是一种可以作为赋值语句左值的特殊表达式,因此也得到引用类型。decltype((variable))结果永远是引用,declytpe(variable)只有当变量本身是引用时才是引用
    int i; // decltype((i))是int&
    
    • 在C++14的decltype(auto)中,这将导致返回局部变量的引用的隐患
    decltype(auto) f1()
    {
        int x = 0;
        …
        return x; // decltype(x)是int,因此返回int
    }
    decltype(auto) f2()
    {
        int x = 0;
        …
        return (x); // decltype((x))是int&,因此返回了局部变量的引用
    }
    

    04 查看推断类型的方法

    • 使用IDE时将鼠标停留在变量上
    鼠标放在x上
    • 利用编译诊断信息,比如写一个类模板声明但不定义,用这个模板创建实例时将出错,编译将提示错误原因
    template<typename T> class TD;
    
    TD<decltype(x)> xType; // 未定义类模板,错误信息将提示x类型
    // 比如对int x报错如下
    error C2079: “xType”使用未定义的 class“TD<int>”
    
    • 使用type_id和std::type_info::name,但这不太可靠,因为引用会被忽略
    template<typename T>
    void f(T& param)
    {
        using std::cout;
        cout << "T = " << typeid(T).name() << '\n';
        cout << "param = " << typeid(param).name() << '\n';
    }
    
    • 使用Boost.TypeIndex可以得到精确类型
    #include <boost/type_index.hpp>
    
    template<typename T>
    void f(const T& param)
    {
        using std::cout;
        using boost::typeindex::type_id_with_cvr;
        cout << "T = " << type_id_with_cvr<T>().pretty_name() << '\n';
        cout << "param = " << type_id_with_cvr<decltype(param)>().pretty_name() << '\n';
    }
    

    相关文章

      网友评论

        本文标题:【Effective Modern C++(1)】类型推断

        本文链接:https://www.haomeiwen.com/subject/uzwdqqtx.html