Bootstrap

Modern Effective C++:Item 6 auto推导若非己愿,使用显式类型初始化惯用法

前置:vector<bool>

首先vector<bool>并不是一个通常意义上的vector容器。早在C++98的时候,就有vector< bool>,但是因为当时为了考虑到节省空间,所以vector<bool>里面不是一个Byte储存的,它是一个bit一个bit储存的。

因为没有直接去给一个bit来操作,所以用operator[]的时候,正常容器返回的应该是一个对应元素的引用,但是对于vector< bool>实际上访问的是一个"proxy reference"而不是一个"true reference",返回的是std::vector< bool>:reference类型的对象。

一般情况:

vector c {false, true, false, true, false};
bool b=c[0];
auto d=c[0];

b的初始化暗含一个隐式的类型转换。对于d,类型并不是bool,而是一个vector<bool>中的一个内部类。此时如果修改d的值,c中的值也会跟着修改。如果c被销毁,d就会变成一个悬垂指针,再对d操作就属于未定义行为。所以对于容器一些基本的操作它并不能满足,诸如取地址给指针初始化操作,没有办法给单一bit来取地址或者引用。

vector c{ false, true, false, true, false };
bool &tmp = c[0]; //错误,不能编译,对于引用来说,因为c[0]不是一个左值。
bool *p = &c[0]; //错误,不能编译,因为无法将一个临时量地址给绑定到指针。

std::vector<bool>的operator[] 实际上返回的是一个 std::vector<bool>::reference 类型的对象,这是一个代理类,它模仿了 bool& 的行为。这个代理类提供了对单个位的操作能力,包括读取和设置位的值。std::vector<bool>::reference 是 std::vector<bool> 特化中使用的一个代理类。由于 std::vector<bool> 以位的形式存储布尔值,导致了与普通 std::vector<T> 行为上的差异。std::vector<bool> 不能像其他 std::vector<T> 那样直接返回 bool& 类型的引用,因为 C++ 语言不允许对单独的位进行引用。为了解决这个问题,std::vector<bool> 使用了一个代理类 std::vector<bool>::reference 来模拟 bool& 的行为。std::vector<bool>::reference 包含一个指向内部位数组的指针:这个指针指向存储布尔值的整数。

一个表示位位置的索引:这个索引表示具体的位在整数中的位置。

#include <iostream>
#include <vector>
int main() {
    std::vector<bool> v = {true, false, true, false};
    // 获取第 2 个元素的引用
    std::vector<bool>::reference ref = v[2];
    // 打印第 2 个元素的值
    std::cout << "v[2] = " << ref << std::endl;  // 输出: v[2] = 1
    // 修改第 2 个元素的值
    ref = false;
    // 再次打印第 2 个元素的值
    std::cout << "v[2] = " << v[2] << std::endl;  // 输出: v[2] = 0
    return 0;
}

前置:代理类

待整理

使用 auto 时的问题

当使用 auto 来声明变量并初始化时,编译器会根据初始化表达式的类型来推断变量的类型。对于 std::vector<bool> 的 operator[],auto 会推断出 std::vector<bool>::reference 而不是 bool。这可能导致问题,特别是当 std::vector<bool> 是一个临时对象时,std::vector<bool>::reference 中的指针会变成悬空指针,导致未定义行为。

调用features将返回一个std::vector<bool>临时对象,这个对象没有名字,为了方便我们的讨论,我这里叫他temp。operator[]在temp上调用,它返回的std::vector<bool>::reference包含一个指向存着这些bits的一个数据结构中的一个word的指针(temp管理这些bits),还有相应于第5个bit的偏移。highPriority是这个std::vector<bool>::reference的拷贝,所以highPriority也包含一个指针,指向temp中的这个word,加上相应于第5个bit的偏移。在这个语句结束的时候temp将会被销毁,因为它是一个临时变量。因此highPriority包含一个悬置的指针,如果用于processWidget调用中将会造成未定义行为。

processWidget(w, highPriority); //未定义行为.highPriority包含一个悬置指针.

下面这个没看懂。

// 模拟 std::vector<bool>::reference 类
class vector_bool_reference {
  friend class vector_bool;
  //私有成员
  unsigned char* ptr;//指向存储位的整数的指针
  unsigned char index;//位的位置
  //私有构造函数
  vector_bool_reference(unsigned char* p, unsigned char i) noexcept:ptr(p),index(i) {}
public:
  //析构函数
  ~vector_bool_reference() = default;
  //转换为 bool
  operator bool() const noexcept {
    return (*ptr & (1 << index)) != 0;
  }
  //从 bool 赋值
  vector_bool_reference& operator=(const bool x) noexcept {
    if (x) {
      *ptr |= (1 << index);
    } else {
      *ptr &= ~(1 << index);
    }
    return *this;
  }
  //从另一个 reference 赋值
  vector_bool_reference& operator=(const vector_bool_reference& x) noexcept {
    *ptr = ((*ptr & ~(1 << index)) | (static_cast<bool>(x) << index));
    return *this;
  }
  // 翻转位的值
  void flip() noexcept {
    *ptr ^= (1 << index);
  }
};
class vector_bool {
  unsigned char* data;  // 指向存储位的数组
  size_t size_;         // 位的数量
public:
  vector_bool(size_t n) : data(new unsigned char[(n + 7) / 8]), size_(n) {
    for (size_t i = 0; i < (n + 7) / 8; ++i) {
      data[i] = 0;
    }
  }
  ~vector_bool() {
    delete[] data;
  }
  //获取位的引用
  vector_bool_reference operator[](size_t pos) {
    return vector_bool_reference(&data[pos / 8], pos % 8);
  }

为了避免这些问题,建议显式地将结果转换为 bool:

bool highPriority = features(w)[5];

或者使用 static_cast 显式转换:

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

这样可以确保 highPriority 是一个真正的 bool 变量,而不是一个代理类实例。

一些代理类被设计于用以对客户可见。比如std::shared_ptr和std::unique_ptr。其他的代理类则或多或少不可见,比如std::vector<bool>::reference就是不可见代理类的一个例子,还有它在std::bitset的胞弟std::bitset::reference。

在不可见代理类里一些C++库也是用了表达式模板(expression templates)。

这些库通常被用于提高数值运算的效率。给出一个矩阵类Matrix和矩阵对象m1,m2,m3,m4,举个例子,这个表达式

Matrix sum = m1 + m2 + m3 + m4;

可以使计算更加高效,只需要使让operator+返回一个代理类代理结果而不是返回结果本身。也就是说,对两个Matrix对象使用operator+将会返回如Sum<Matrix, Matrix>这样的代理类作为结果而不是直接返回一个Matrix对象。在std::vector<bool>::reference和bool中存在一个隐式转换,同样对于Matrix来说也可以存在一个隐式转换允许Matrix的代理类转换为Matrix,这让表达式等号“=”右边能产生代理对象来初始化sum。(这个对象应当编码整个初始化表达式,即类似于Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>的东西。客户应该避免看到这个实际的类型)作为一个通则,不可见的代理类通常不适用于auto。这样类型的对象的生命期通常不会设计为能活过一条语句,所以创建那样的对象你基本上就走向了违反程序库设计基本假设的道路。std::vector<bool>::reference就是这种情况,违反这个基本假设将导致未定义行为。

因此想避开这种形式的代码:

auto someVar = expression of "invisible" proxy class type;

显式类型初始器惯用法使用auto声明一个变量,然后对表达式强制类型转换(cast)得出你期望的推导结果。例:

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

features(w)[5]还是返回一个std::vector<bool>::reference对象,就像之前那样,但是这个转型使得表达式类型为bool,然后auto才被用于推导highPriority。在运行时,std::vector<bool>::operator[]返回的std::vector<bool>::reference执行它支持的向bool的转型,在这个过程中指向std::vector<bool>的指针已经被解引用。这避开了我们之前的未定义行为。对于Matrix来说,显式类型初始器惯用法如下:

auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

;