Skip to content

Effective Modern C++ 笔记

2312字约8分钟

cpp程序语言

2025-01-14

类型推导

item1 理解模版类型推导

  • 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
  • 对于通用引用的推导,左值实参会被特殊对待
  • 对于传值类型推导,const和/或volatile实参会被认为是non-const的和non-volatile
  • 在模板类型推导时,数组名或者函数名实参会退化为指针,除非它们被用于初始化引用

考虑伪代码的函数模板:


template<typename T>
void f(ParamType param);

// 调用
f(expr);

在编译期间使用 expr 进行两个类型推导:一个是针对 T 的,一个是针对 ParamType,后者通常是包含修饰的 T。

  • 情景一:ParamType 是一个指针或引用,但不是通用引用

    template<typename T>
    void f(T& param);
    
    int x = 27;        // f(x)  T: int; param: int&
    const int cx = x;  // f(cx) T: const int; param: const int&
    const int& rx = x; // f(rx) T: const int; param: const int&
    									 // 即使 rx 是个引用,但 T 在推导中忽略引用性,右值引用同理
    
    template<typename T>
    void f(const T& param); // reference-to-const
    int x = 27;        // f(x)  T: int; param: const int&
    const int cx = x;  // f(cx) T: int; param: const int&
    const int& rx = x; // f(rx) T: int; param: const int&
    									 // 即使 rx 是个引用,但 T 在推导中忽略引用性,右值引用同理
    
    template<typename T>
    void f(T* param);
    int x = 27;         // f(&x) T: int; param: int*
    const int *px = &x; // f(px) T: const int; param: const int*
  • 情景二:ParamType 是一个通用引用(通用引用,模板 T,声明形式为 T&&)

    template<typename T>
    void f(T&& param);
    
    // 通用引用考虑传参的参数是左值还是右值
    // - 左值:那么 T 和 param 都会被推导为左值引用
    // - 右值:那么 T 会按照情景一进行推导,param 为相应的右值引用
    int x = 27;         // f(x)  T: int&; param: int&
    const int cx = x;   // f(cx) T: const int&; param: const int&
    const int &rx = cx; // f(rx) T: const int&; param: const int&
    // f(27) T: int; param: int&&
  • 情景三:ParamType 既不是指针也不是引用(传值)

    template<typename T>
    void f(T param);
    
    // param 传参为拷贝新对象,既然是新对象,则引用、顶层const、volatile 都会被忽略
    int x = 27;
    const int cx = x;
    const int& rx = cx; // T 和 param 的类型都是 int
  • 数组实参与函数实参:数组和函数在传参时会退化成指针,但在接收引用 param 时可以接收指向数组的引用。

    template<typename T>
    void f(T& param);
    
    int arr[] = {1, 2, 3}; // f(arr) T: int[3], param int (&)[3]
    // 通过接收数组的引用,还能创建模板函数来推导数组的大小
    template<typename T, std::size_t N>
    constexpr std::size_t arraySize(T (&)[N]) noexcept {
      return N;
    }

item2 理解 auto 类型推导

  • auto类型推导通常和模板类型推导相同,但是auto类型推导假定花括号初始化代表std::initializer_list,而模板类型推导不这样做
  • 在C++14中auto允许出现在函数返回值或者lambda函数形参中,但是它的工作机制是模板类型推导那一套方案,而不是auto类型推导
  • auto 推导与 item1 模板推导大部分一致,三个情景稍微修改就能使用 auto

    • 情景一:类型说明符是一个指针或引用但不是通用引用
    • 情景二:类型说明符一个通用引用
    • 情景三:类型说明符既不是指针也不是引用
  • 但在有初始化列表时表现不一致

    auto x = {1, 2, 3}; // x 的类型为 std::initializer_list<int>
    
    template<typename T>
    void f(T param); // 等价形式无法推导 f({1, 2, 3})
    
    // 如果模板需要推导此类需要
    template<typename T>
    void f(std::initializer_list<T> initList); // f({1, 2, 3}) T 为 int,initList 为 std::initializer_list<int>
    
    // 对于 lambda 中的 auto 推导在实际上是模板推导那套而不是 auto 类型
    auto createInitList()
    {
        return { 1, 2, 3 };         //错误!不能推导{ 1, 2, 3 }的类型
    }
    std::vector<int> v;
    // …
    auto resetV = 
        [&v](const auto& newValue){ v = newValue; };        //C++14
    
    resetV({ 1, 2, 3 });            //错误!不能推导{ 1, 2, 3 }的类型

item3 理解 decltype

  • decltype总是不加修改的产生变量或者表达式的类型。
  • 对于T类型的不是单纯的变量名的左值表达式,decltype总是产出T的引用即T&
  • C++14支持decltype(auto),就像auto一样,推导出类型,但是它使用decltype的规则进行推导。
  • decltype 相比 auto 或者模板推导,它只是简单的返回名字或者表达式的类型。

  • 用一个完善的例子理解 lambda 中的 decltype:

    template<typename Container, typename Index>    //最终的C++14版本
    decltype(auto) // decltype(auto)理解: auto 代表返回值需要推导,decltype 代表按照 decltype 的方式来推导返回值
    authAndAccess(Container&& c, Index i) // 通用引用可以传递右值
    {
        authenticateUser();
        return std::forward<Container>(c)[i]; // forward 实现完美转发
    }

    C++11 需要显示指定函数返回类型:

    
    template<typename Container, typename Index>    //最终的C++11版本
    auto
    authAndAccess(Container&& c, Index i)
    ->decltype(std::forward<Container>(c)[i])
    {
        authenticateUser();
        return std::forward<Container>(c)[i];
    }
  • 如果一个不是单纯变量名的左值表达式的类型是T,那么decltype会把这个表达式的类型报告为T&

    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& UB!!
    }

item4 学会查看类型推导结果

  • 类型推断可以从IDE看出,从编译器报错看出,从Boost TypeIndex库的使用看出
  • 这些工具可能既不准确也无帮助,所以理解C++类型推导规则才是最重要的

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

auto

item5 优先考虑 auto 而非显式类型声明

  • auto变量必须初始化,通常它可以避免一些移植性和效率性的问题,也使得重构更方便,还能让你少打几个字。
  • 正如Item26讨论的,auto类型的变量可能会踩到一些陷阱。

auto 的几个好处:

  • auto 变量必须要初始化,所以避免了忘记初始化带来的 ub

  • 闭包的类型只有编译器知道,而 auto 能够表示。在 C++14 中,形参也可以使用 auto。

    • 如果不使用 autostd::function 确实能够将闭包存放在该对象中,但用此方法语法冗长、重复写形参、实例化的 function 对象额外空间,有可能还会在堆上分配空间
  • 可以避免一些可移植性问题,例如 std::vector<int>::size_type 在 win32 是 32 位,在 win64 是 64 位

  • 在某些情况更有效率。考虑哈希表的 range-base 的遍历

    std::unordered_map<std::string, int> m;
    
    for (const std::pair<std::string, int>& p: m) {
      // do something
      // 实际上 m 里的 pair 是 std::pair<const std::string, int> 的
      // 编译器会拷贝一个临时对象使得这个语法有效
      // 即类型不匹配会降低效率
    }
    
    // 而 auto 避免了此问题
    for (const auto& p: m) {
      // do something
    }

当然,auto 是可选项,在 item2 和 6 中展示了有些坑。如果认为 auto 影响可读性,那可以好好配置一下 IDE。

item6 auto 推导不顺心则使用显式类型

auto 在某些时候会失去效用,例如隐式的代理类,以及想要的隐式转换。

  • 隐式代理类的错误推导:以 std::vector<bool>::operator[] 为例,C++ 为其实现了一个特化版本,因为内存是按字节寻址的,bool 不需要一个字节存储,所以底层实质上是位图,那么 operator[]自然而然不是返回一个引用,而是使用了一个代理类来实现类似其他std::vector::operator[] 的功能。image-20250116160441665

    具体而言,代理类存储了一个原序列的指针和一个掩码,在调用 operator[] 时生成一个代理类对象和其掩码,代理类实现了 operator bool() 的函数,即 static<bool>(xxx) image-20250116161402516

    image-20250116161046893

​ 那么考虑下列代码,

std::vector<bool> features(const Widget& w);

bool bit = features(w)[5]; // 类型为 std::vector<bool> 的临时对象 (features(w))[5] => reference(temp, mask(5)) => static_cast<bool>(reference)

auto bit_ref = features(w)[5]; // 类型为 std::vector<bool> 的临时对象 (features(w))[5] => reference(temp, mask(5))
// 这里 bit_ref 类型为 bit_reference,其中的 temp 指针和 mask 被拷贝至此
bool bit_ = static_cast<bool>(bit_ref); // 此时会访问 *seg & mask,但 seg 指针是 std::vector<bool> 临时对象的指针,在上一个语句完成后被析构了,所以这里是悬挂指针,未定义行为

// 所以说正确形式应该为
auto bit__ = static_cast<bool>(features(w)[5]);
  • 有时候用会使用显式的类型说明,例如 float a = 1.; 来隐式地表示“我要降低精度”,这种情况用 auto a = static_cast<float> 代替,可读性会更高,可理解性也更高