C++ 的 auto 与 decltype 类型推导

https://cntransgroup.github.io/EffectiveModernCppChinese/1.DeducingTypes/item2.html

https://cntransgroup.github.io/EffectiveModernCppChinese/1.DeducingTypes/item3.html

https://cntransgroup.github.io/EffectiveModernCppChinese/2.Auto/item5.html

https://cntransgroup.github.io/EffectiveModernCppChinese/2.Auto/item6.html

auto 类型推导

auto 与模板类型推导

在 C++ 中,auto 类型推导通常和模板类型推导相同,也分为:

auto x;

auto& x;
const auto& x;

auto&& x;
const auto&& x;

但是只有一个例外:auto 类型推导假定花括号初始化代表 std::initializer_list,而模板类型推导不这样做

在模板类型推导中,如果出现下面的代码:

auto x = { 11, 23, 9 };                         // x 的类型是 std::initializer_list<int>

template<typename T>                            // 带有与 x 的声明等价的
void f(T param);                                // 形参声明的模板

f({ 11, 23, 9 });                               // 错误!不能推导出 T
f<std::initializer_list<int>>({ 11, 23, 9 });   // 正确,直接指定 T = std::initializer_list<int>

上面的代码会直接报错,因为花括号不能推导出 std::initializer_list

而在 auto 时则不同,x 可以通过 auto 推导出 std::initializer_list<int> 类型

此外,C++ 14 新增的 lambda 表达式的 auto 语法实际上是模板类型参数的语法糖,使用模板类型推导那一套方案

使用 auto 进行返回值类型推导

C++ 支持后置返回类型,如:

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

此时,返回值的类型与 c[i] 的类型(有时 operator[] 的返回值并不是引用,如 std::vector 可能返回一个 wrapper 新对象)完全相同

但是如果去掉 decltype 仅使用 auto 进行自动返回值类型推导(C++ 14),如:

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

std::vector<int> vec{1, 2, 3};
authAndAccess(vec, 1) = 3;           // 无法通过编译,它会返回右值而不是左值引用

此时使用 auto 进行推导,它会进行退化!

优先考虑 auto 而不是显式类型推导

可以避免没有初始化的情况,因为 auto 必须显式初始化

可以使用 auto 来让编译器推导闭包的类型(即 lambda 表达式,它作为一个匿名对象,它的类型只有编译器知道)

可以减少一些移植性的问题,例如:

  • std::vector<>::size() 返回一个 std::vector<>::size_type 类型,它与平台和编译器有关,32 位机器上是 4 字节,而 64 位机器上是 8 字节。如果我们直接用 unsigned 的话会有问题,直接使用那一串类型别名的话也不好写,可以直接用 auto

  • 很少有人注意到 std::unordered_map<K, V> 中的 pair 类型为 std::pair<const K, V>,如果写出下面的代码:

    std::unordered_map<std::string, int> m;
    for(const std::pair<std::string, int>& p : m) {    // 1
    }
    
    std::pair<const std::string, int> p;
    const std::pair<const std::string, int>& rp = p;   // 2
    

    那么它实际上在 1 和 2 处,会进行一个隐式类型转换,将 std::pair<const K, V> 先拷贝一份,变成 std::pair<K, V>(纯右值),然后由于 const 引用可以引用纯右值,所以直接实质化了,这样我们发现修改 p.second 并不会修改 m 里面的值。使用 auto 就不会出现这种错误:

    std::unordered_map<std::string, int> m;
    for(const auto& p : m) {
    }
    
    std::pair<const std::string, int> p;
    const auto& rp = p;
    

有时 auto 并不会推导出我们期望的类型

std::vector<bool>std::vector<> 的一个特化,STL 特地为我们准备了这个坑。它将内部的 bool 值按照进行存储,而不是每一个 bool 占据一个字节

我们看看这个 std::vector<bool> 内部是怎么实现 operator[] 的(简化版):

struct Bit_reference {
    using Bit_type = unsigned long;
    
    Bit_type* p;
    Bit_type mask;
    
    Bit_reference& operator=(bool x) noexcept {
        if (x) *p |= mask;
        else *p &= ~mask;
        
        return *this;
    }
};


template<typename Alloc>
class vector<bool, Alloc> : protected Bvector_base<Alloc> {
    ...
    using reference = Bit_reference;
    using size_type = std::size_t;
    using iterator = Bit_iterator;
    ...
    
    [[nodiscard]] constexpr iterator begin() noexcept {
        // this->start.p 是第一个字节的 iterator,第二个参数表示是第几个比特位
        return iterator(this->start.p, 0);
    }
    
    [[nodiscard]] constexpr reference operator[](size_type n) {
        return begin()[n];
    }
};

可以看到,它返回的并不是 bool&,而是 Bit_reference&,这被叫做代理对象

如果我们写出下面的代码:

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

Widget w;
auto highPriority = features(w)[5];
processWidget(w, highPriority);

上面的代码中,auto 被推导为 Bit_reference

features() 返回一个纯右值,我们调用了 operator[],此时实质化了 std::vector<bool>,但是之后由于 auto 进行了退化,它拷贝了一份,生命周期就结束了(因为已经没有引用绑定它了),它可能会被析构,这是个未定义行为

此时,Bit_reference 中的指针就悬空了!

同样的,在一些数学库中,可能会使用代理对象来延迟计算,例如:

Matrix sum = m1 + m2 + m3 + m4;

这时也可能会出现问题

我们可以使用强制类型转换来保证得到我们想要的结果,如:

auto highPriority = static_cast<bool>(features(w)[5]);

decltypedecltype(auto)

decltype 不进行退化,它的作用是进行类型推导。它可以与 auto 混用,即 decltype(auto),进行不退化的类型推导

上面的例子可以改成:

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

std::vector<int> vec{1, 2, 3};
authAndAccess(vec, 1) = 3;           // 正确

decltype(x)decltype((x)) 的区别

x 表示一个变量,而 (x) 则表示一个表达式

在 C++ 11 中规定表达式 (x) 是一个左值,因此这两个 decltype 可能一个是右值一个是左值,如:

int x = 0;

decltype(x) y = x;     // int
decltype((x)) z = x;   // int&

decltype(auto) f() {
    int x = 0;
    return (x);        // decltype((x)) 是 int&,所以 f 返回 int&
    return x;          // decltype(x) 是 int,所以 f 返回 int
}