decltype()

作者: 部洪波 | 来源:发表于2022-03-11 00:16 被阅读0次

    摘要

    本文是关于 decltype() 的学习笔记。各种版权都属于原作者。如果存在侵权行为,请通知本人删除。

    decltype()

    这是个什么东西?对于指定的名字或者表达式decltype() 能给出它的类型,就仿佛 sizeof() 能告诉我对象的尺寸。

    名字或表达式(举例) decltype 的用法 结果类型
    const int i = 0; decltype(i) //普通变量 const int // 变量的类型
    bool f(const Widget& w) decltype(w) //函数形参
    decltype(f) //函数名称
    const Widget& //形参的类型
    bool(const Widget&) //函数的类型
    struct point {
    int x, y;
    };
    decltype(Point::x) //结构体成员
    decltype(Point::y)
    int
    int
    Widget w decltype(w) //类的实例对象 Widget //对象所属的类
    if ( f(w) ) ... decltype(f(w)) //函数调用表达式 bool //函数的返回类型
    template<typename T>
    class vector {
    public:
    ...
    T& operator[](std::size_t index);
    ...
    };
    vector<int> v;
    ...
    if (v[0]==0)...
    std::vector 的简化版
    decltype(v)//模板类的实例划对象名字

    decltype(v[0])// 取元素的表达式
    vector<int>

    int&

    从这张表大概可以感受到decltype()的能力和推导规则,基本上就是有一说一。

    貌似可以翻译为拓印类型或者叫类型拓印,类似于中国传统的拓印技术。

    最佳实践:代码案例研究

    假设我们想要撰写一个函数,其形参包括一个容器,支持方括号下标语法(即[])和一个下标,并会在返回下标操作结果前进行用户验证。函数的返回值型别必须与下标操作结果的返回值型别相同。

    下标操作返回的是对容器内元素的引用。

    版本一 C++11 (v1.0)

    template<typename Container, typename Index>
    auto authAndAccess( Container& c, Index i)
      -> decltype(c[i]) // 学生以为这种写法着实难看, 不是C++的传统风格。
    {
        authenticateUser();
        return c[i];
    }
    

    这里的auto与类型推导没有任何关系,它仅仅是C++11中的函数的返回类型尾序语法的指示(trailing return type syntax),即 返回类型放在形参列表之后(在 -> 之后)。尾序返回值的好处是可以使用形参。

    版本二 C++14(有缺陷 v0.1)

    template <typename Container, typename Index)
    auto authAndAccess( Container &c, Index i ) // c++14
    {
        authenticationUser();
        return c[i];  // 函数的返回类型根据 c[i] 推导,有点问题
    }
    

    auto 这里不再是个简单的指示了,而是要求编译器推导函数的返回类型,即c[i]类型。

    还有点儿问题,在哪儿呢?看一段使用它的代码:

    std::deque<int> d;
    // ...
    authAndAccess(d, 5) = 10; 
    // 验证用户,并返回d[5],然后将其赋值为10(因为
    // 从逻辑上函数返回的似乎是int&引用,所以使用者认为可以赋值修改),
    // 但这个代码无法通过编译
    

    原来,在模板类型推导规则中,如果是在初始化表达式中,那推导结果类型中的引用修饰就被去掉了。编译时会爆出如下的错误:

    [ ~/workspace/temp ] %  g++ -std=c++14 -Wall -o decltype decltype.cc
    decltype.cc:20:23: error: expression is not assignable
      authAndAccess(d, 5) = 10;
      ~~~~~~~~~~~~~~~~~~~ ^
    

    剥掉了引用属性的返回类型是一个int型右值,无法被赋值修改。

    如果我把函数调用放到赋值的右边,并用一个引用型变量接收,看看编译器报告的错误信息:

    int & v = authAndAccess(d, 5)
    //           ^^^^^^^^^^^^^^^^
    // error: non-const lvalue reference to type 'int' 
    // cannot bind to a temporary of type 'int'
    

    从消息中可以清晰看到,函数返回类型是int,它的引用属性被干掉了。

    版本三 用 C++14 实现(v1.0)

    如何才能同时做到:

    • 让编译器自动推导函数返回类型。
    • 让编译器使用decltype()的推导规则(而不是auto的推导规则)。
      答案好像也很简单:鱼和熊掌一锅炖了……
    template<typename Container, typename Index>
    decltype(auto) // C++14, 现在函数的返回类型真的跟c[i]一样了!
    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的类型就是 Widget&
    

    怎么来理解 decltype(auto) 呢?总而言之就是告诉编译器这里需要做自动类型推导,并且推导规则就用 decltype()的规则。

    版本四:C++11/14 v2.0 面对右值引用的代码缺陷

    缺陷与如何使用这个 authAndAccess() 时的实参有关系。

    第(1)种情况:在这个函数调用表达式之前,第1个入参即容器已经存在,并且在表达式执行完毕后还继续存在。例如:

    std::deque<int> d;
    // ... 一些处理
    auto s = authAndAccess(d, 5);
    s = 10;
    

    之前的版本处理的很好。

    第(2)种情况:容器入参是在函数调用表达式执行时产生的,表达式执行完毕后对象就析构了。例如:

    std::deque<std::string> makeStringDeque();  // 工厂函数
    
    auto s = authAndAccess(makeStringDeque(), 5);
    s += "ok"; 
    

    第2行的函数调用表达式种,第1个参数是由工厂函数生成的一个临时对象;表达式执行完毕后,这个临时对象就析构了。像这种对临时对象的引用,一般叫右值引用。

    第3行代码现在还正确吗?注意,s 现在成了一个悬空引用。为什么?当右值对象析构时,会一并析构其元素对象(即 std::string)。

    解决方法有两种:(1)利用函数重载,一个版本用于左值引用,一个版本用于右值引用。(2)使用“万能引用”并配合“完美转发”。

    C++14版本

    template<typename Container, typename Index>
    decltype(auto)
    authAndAccess(Container&& c, Index i)
    {
        authenticateUser();
        return std::forward<Container>(c)[i];
    }
    

    C++11版本

    template<typename Container, typename Index>
    auto authAndAccess( Container&& c, Index i )
    -> decltype(std::forward<Container>(c)[i])
    {
        authenticateUser();
        return std::forward<Container>(c)[i];
    }
    

    关于万能引用和完美转发,另有学习专章。

    一种神奇的坑

    一个看似无关紧要的返回值写法上的小改动,就会影响道函数的类型推导结果:

    decltype(auto) f1()
    {
      int x = 0;
      // ...
      return x; // decltype(x)是int,因此f1返回的是int
    }
    
    decltype(auto) f2()
    {
      int x = 0;
      // ...
      return (x); // decltype((x))是int&,所以f2返回的是int&
    }
    

    对于f2,其返回的是对局部变量的引用,这是一种未定义行为。

    教训:使用 decltype(auto) 要极其小心翼翼。

    查看类型推导结果的方法

    (一)利用 IDE 编辑器和编译器,实现静态的类型查看。

    (二)利用 std::type_info::name 结果有时不太准确。。

    (三)利用 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;
        
        // 显示 T 的类型
        cout << "T =    "
             << type_id_with_cvr<T>().pretty_name()
             << '\n';
             
       cout << "param = "
            << type_id_with_cvr<decltype(param)>().pretty_name()
            << '\n';
    }
    

    再看看用户端代码:

    std::vector<Widget> createVec();  // 工厂函数
    
    const auto vw = createVec();  // 使用工厂函数返回值初始化 vw
    
    if ( !vw.empty() ) {
        f(&vw[0]);
        // ...
    }
    

    输出是:

    T =     Widget const*
    param = Widget const* const&
    

    本文内容来自多种资料,包括但不限于《Effective Modern C++》,《C++ Primer》(第5版),《C++程序设计语言》(第四版)等。

    相关文章

      网友评论

        本文标题:decltype()

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