avatar

Catalog
Effective Modern C++ 笔记(1):Intro & Deducing Types

Introduction

  • argument 是实参,可以是左值也可以是右值;

    parameter 是形参,一定是左值。

  • basic exception safety guarantee 是指发生异常后程序的不变量仍然得到保证以及没有资源被泄露;

    strong exception safety guarantee 是指发生异常后程序的状态要和调用这个抛出异常的函数之前一样。

Chapter 1 Deducing Types

  • C++ 98 中类型推导只用于函数模板的类型参数,C++ 11 中新引入了两个使用类型推导的场景:autodecltype

Item 1 Understand template type deduction

考虑这样的场景:

c++
1
2
3
4
template<typename T>
void f(ParamType param); // ParamType 是 T 或者 T 和一些修饰符(比如 const,&,*)的组合

f(expr);

类型推断大致分为三类:

  • ParamType 是指针或引用(但不是 universal reference):如果 expr 是引用的话,去掉 &,然后将 expr 的类型和 ParamType 相比,得到 TParamType 包含 T)。
  • ParamType 是 universal reference:即 ParamTypeT&&。如果 expr 是左值,T 被推断为左值引用,然后根据 reference collapsing 将 ParamType 也折叠成左值引用;如果 expr 是右值,T 被推断为正常类型(不含 &),expr 为右值引用。
  • ParamType 不是指针也不是引用:忽略 expr&, const 以及 volatile,然后和 ParamType 相比得到 T。需要注意的是,对于 exprconst int *const 这种情况,只能忽略后面那个 const

对于 expr 是数组的情况(例如 int[5]):

  • 如果 ParamType 不是引用,T 被推断为 int *(typeid 为 Pi)。
  • 如果 ParamType 是引用,T 被推断为 int(&)[5](typeid 为 A5_i)。

对于 expr 是函数的情况(例如 int(char)):

  • 如果 ParamType 不是引用,T 被推断为 int (*)(char)(typeid 为 PFicE)。
  • 如果 ParamType 是引用,T 被推断为 int (&)(char)(typeid 为 FicE)。

Item 2 Understand auto type deduction

auto type deduction 和 template type deduction 类似,只有一点不同。

  • 先说相同点,考虑这样的场景:

    c++
    1
    2
    3
    auto x = 27;
    const auto cx = x;
    const auto& rx = x;

    定义变量时的 type specifier(该处的 auto, const auto, const auto&)即相当于 template type deduction 中的 ParamType,而 auto 本身则相当于 T

    也就是说,const auto& rx = x; 所做的推断和以下 template type deduction 所做的推断是一样的:

    c++
    1
    2
    3
    4
    template<typename T>
    void func_for_rx(const T& param);

    func_for_rx(x);
  • 相同点说完了,然后是不同点:在定义变量时如果用花括号进行初始化,auto 会被推断为 initializer_list;而对于模板函数来说,不允许对花括号括起来的一组值进行类型推断。

    此外,C++14 中还引入了 auto 作为函数返回值和 auto 作为 lambda 表达式入参这两种机制。在这两种情形中,auto 实际使用 template type deduction 的原则进行类型推断而非 auto type deduction,即返回值和 lambda 表达式的入参不能是花括号括起来的一组值。

Item 3 Understand decltype

  • decltype 是一个关键字,它接受一个变量名或表达式作为参数,返回其类型。这种不做任何修改,直接返回参数类型的类型推断方式也叫 decltype type deduction。

    值得一提的是 decltype ((x)) 这种用法(x 是变量名),推断出的类型为左值引用,因为参数是左值表达式而非变量名,但这不能算特例,只是反直觉而已。

  • 把函数的参数传递给 decltype,可以将 decltype 推断出的类型作为返回值写在函数参数列表之后,这种用法叫 trailing return type(写在参数列表之后是因为 decltype 需要用到函数的参数,而这些参数在参数列表中定义):

    c++
    1
    2
    3
    4
    5
    6
    template <typename Container, typename Index>
    auto authAndAccess(Container& c, Index i) -> decltype(c[i])
    {
    authenticateUser();
    return c[i];
    }

    而在 C++14 中,可以省略 trailing return type,用之前提到过的 template type deduction (auto 作为返回值的情况)来推断返回值:

    c++
    1
    2
    3
    4
    5
    6
    template <typename Container, typename Index>
    auto authAndAccess(Container& c, Index i)
    {
    authenticateUser();
    return c[i];
    }

    但是这就有一个问题,c[i] 返回的一般都是左值引用 T&,而 template type deduction 中会忽略掉 &,所以函数的返回值就变成了 T,这与 decltype(c[i]) 不一致。为了解决这个问题,C++14 引入了 decltype(auto),即使用 decltype type deduction 来推断返回值类型:

    c++
    1
    2
    3
    4
    5
    6
    template<typename Container, typename Index>
    decltype(auto) authAndAccess(Container& c, Index i)
    {
    authenticateUser();
    return c[i];
    }
  • decltype(auto) 还可以像 auto 那样用于定义变量,区别是前者使用 decltype type deduction,而后者使用 auto type deduction。

Item 4 Know how to view deduced types

作者提供了三种查看类型推断结果的方法:分别在开发时刻(编辑代码时),编译时刻以及运行时刻。

  • 开发时刻:利用 IDE 提供的功能,通常将光标悬停在变量名上就能看到其类型。

  • 编译时刻:利用未定义的模板,通过编译器的报错信息获得变量的类型:

    c++
    1
    2
    3
    4
    5
    6
    7
    8
    template<typename T>
    class TD;

    const int theAnswer = 42;
    auto x = &theAnswer;

    // error: aggregate 'TD<const int *> xType' has incomplete type and cannot be defined
    TD<decltype(x)> xType;

    注意这里类型推断使用的原则是 auto type deduction 的 case 3(即 ParamType 不是指针也不是引用),exprconst int *,虽然含有 const,但是是在 * 之前,所以不能忽略(即新定义的指针本身的常量性由 ParamType 中是否含有 const 决定,而被指资源的常量性则由 expr 中是否含有 const 决定)。

  • 运行时刻:利用 typeidtype_infoname 方法获得变量的类型:

    c++
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template<typename T>
    void f(const T& param)
    {
    cout << "T = " << typeid(T).name() << '\n';
    cout << "param = " << typeid(param).name() << '\n';
    }

    std::vector<Widget> createVec();
    const auto vw = createVec();

    if (!vw.empty()) {
    f(&vw[0]);
    }

    输出的结果是 compiler-dependent 的,在 GNU 和 Clang 中是:

    Code
    1
    2
    T =     PK6Widget
    param = PK6Widget

    也就是 const Widget *

    这里可能有几处误解:

    • param (即 &vw[0])类型是 const Widget *,按照 template type deduction 的 case 1,T 应该是 Widget * 才对?

      注意 template type deduction 的 case 1 中所说的 “将 expr 的类型和 ParamType 相比,得到 T”,并不是简单的模式匹配:

      Code
      1
      2
      expr:      const Widget *
      ParamType: const T &

      看上去 T 好像是 Widget *,但是需要考虑到涉及指针时,常量性有两重含义:指针本身和被指资源。ParamType 中的 const 是说 param 本身(也就是指针本身)是 const,而 expr 中的 const 根据它的位置可以得知其含义是被指资源是 const,两者含义并不相同。如果一定要用模式匹配的概念的话,更确切的比对应该是这样:

      类型 常量性 引用
      expr const Widget * × -
      ParamType T &

      可见 expr 有而 ParamType 没有的部分就是 const Widget *,所以 T 就是 const Widget *

      说白了导致这个误解的根本原因是 const intconst int * 两处的 const 虽然位置相似但是语义不同。

    • 刚刚解释了 Tconst Widget * 没错,但是 param 难道不应该是 const Widget *const & 吗?

      这就涉及 type_info::name 方法的机制问题了,该方法实现上要求返回的类型好像经过了一次 pass-by-value 一样(即应用一次 template type deduction 的 case 3),所以 cvr(constvolatilereference)三个性质全部被去掉了(仍然如之前讨论的一样,这里的 const 是变量本身的 const,如果变量是指针,那么被指资源的 const 不在此列),const Widget *const & 也就变成了 const Widget *

Reference

Author: Gusabary
Link: http://gusabary.cn/2020/05/20/Effective-Modern-C++-Notes/Effective-Modern-C++-Notes(1)-Intro&Deducing-Types/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment