【问题标题】:Is it safe to use `for(auto& e : cont)`? What is wrong with vector<bool>?使用 `for(auto& e : cont)` 是否安全? vector<bool> 有什么问题?
【发布时间】:2014-09-28 08:04:09
【问题描述】:

我发现有时使用for (auto&amp; e : cont) 代替普通的for (auto e : cont)(其中cont 是某个容器,例如std::vector)。到目前为止,我已经找到了两个原因:

  1. 引用应该避免复制对象(更快的执行)
  2. 某些课程可能禁止复制(例如std::thread

经过几次测试,我可以看到:

  1. for (auto&amp; e : cont) 适用于除 std::vector&lt;bool&gt; 之外的任何 std::vector&lt;T&gt;
  2. for (cont::reference e : cont) 适用于任何std::vector&lt;T&gt; 包括std::vector&lt;bool&gt;(这里明显的问题是:
    我应该使用它而不是for (auto&amp; e : cont) ?
  3. std::vector&lt;bool&gt; 不被认为是真正的容器,许多人认为它应该重命名(实现很好且有用,但应该有不同的名称,如 bitsetbitfielddynamic_bitset
  4. std::vector&lt;bool&gt; 可以以 for (auto&amp; e : cont) 也可以工作的方式实现(请参阅下面的我的尝试)

这是我用于测试的代码:
(诀窍是使用reference&amp; iterator::operator * () { return *this; }

#include <vector>
#include <iostream>
#include <typeinfo>
using namespace std;

#define USE_STD_VECT_BOOL 0

#if USE_STD_VECT_BOOL
typedef vector<bool> BITS;
#else
typedef class bvect {
    unsigned data; // we could use vector<unsigned> but this is just an examle
    unsigned size;
public:
    bvect(): data(0), size(0) {}
    void push_back(bool value) {
        if(value) data |= (1u<<size);
        size++; }
    class reference {
        friend class bvect;
    protected:
        unsigned& data;
        unsigned  flag;
        reference(unsigned& data, unsigned flag)
        : data(data), flag(flag) {}
    public:
        operator bool() const {
            return data & flag; }
        reference& operator = (bool value) {
            if(value) data |= flag;
            else data &= ~flag;
            return *this; }
    };
    class iterator: protected reference  {
        friend class bvect;
        iterator(unsigned& data, unsigned flag)
        : reference(data, flag) {}
    public:
        typedef bool value_type;
        typedef bvect::reference reference;
        typedef input_iterator_tag iterator_category;
    //  HERE IS THE TRICK:
        reference& operator * () {
            return *this; }
        iterator& operator ++ () {
            flag <<= 1;
            return *this; }
        iterator operator ++ (int) {
            iterator tmp(*this);
            operator ++ ();
            return tmp; }
        bool operator == (const iterator& rhs) {
            return flag == rhs.flag; }
        bool operator != (const iterator& rhs) {
            return flag != rhs.flag; }
    };
    iterator begin() {
        return iterator(data, 1); }
    iterator end() {
        return iterator(data, 1<<size); }
} BITS;
#endif

int main() {
    BITS bits;
    bits.push_back(0);
    bits.push_back(1);
#if !USE_STD_VECT_BOOL
//  won't compile for vector<bool>
    for(auto& a : bits)
        cout << typeid(a).name()
          << " = " << (int)(bool)a
          << endl;
#endif
//  std::_Bit_Reference
    for(BITS::reference a : bits)
        cout << typeid(a).name()
          << " = " << (int)(bool)a
          << endl;
//  few more tests
    for(auto a : bits)
        cout << (int)(bool)a;
    for(bool a : bits)
        cout << (int)(bool)a;
    cout << endl;
}

问题:

  1. 我应该使用for (cont::reference e : cont)而不是for (auto&amp; e : cont)吗?
  2. 这个技巧有什么问题?可以增强它以适应任何用例吗?
    编辑:我在这里指的是bvect::reference&amp; bvect::iterator::operator * () { return *this; }
  3. 可以/应该更改 STL 吗? (参考vector&lt;bool&gt;

反馈:答案和评论:

  1. 使用for (auto&amp;&amp; e : cont)(用于写入)或for (const auto&amp; e : cont)(用于读取/枚举)似乎适用于所有情况。 (感谢 dyp 和 Praetorian)
  2. 使用typename iterator_traits&lt;decltype(begin(cont))&gt;::reference 似乎对数组也有效(cont=boo[2])。 (是的,它很丑陋,但我认为可以使用一些模板别名来缩短它。我想不出需要这样做的反例,所以,目前,这不是解决方案。auto&amp;&amp; 是)
  3. 标准规定iterator::operator * () 必须返回iterator::reference(不是iterator::reference&amp;),但仍然不知道为什么。

最终判决:

auto it = bits.begin();
auto&& e = *it; cout << (bool)e;
it++; cout << (bool)e;
cout << endl;

输出:

10

这绝对是坏事。我们应该坚持标准(iterator::operator * () 必须返回iterator::reference)。 谢谢你:)

【问题讨论】:

  • 您经常听到的准则是:auto&amp;&amp;auto const&amp;,具体取决于您是否要修改元素。
  • typename iterator_traits&lt;It&gt;::reference r = *i; ++i; r = *i; 使用这个迭代器会有奇怪的行为。我不确定它是否满足所有可变 RAIt 要求。
  • @firda 在这种情况下,您违反了*i 应返回reference 而不是reference&amp; 的要求。
  • @firda 表 106 / [iterator.iterators] 在标准或更高版本的草案中。在 cppreference 上:en.cppreference.com/w/cpp/concept/Iterator 这些要求是从Iterator“继承”到InputIterator
  • 这样的static_assert 可能出现在大量模板化的代码中,不是作为一种安全措施,而是为了改进错误消息(à la 概念)。重载解决方案仍然是我能想到的最佳答案,但我怀疑这是基本原理。也许要求太严格了。

标签: c++ c++11 boolean stdvector auto


【解决方案1】:

vector&lt;bool&gt;vector 类模板的特化,它将布尔值存储在位域中以进行空间优化。由于不能返回对位域的引用,vector&lt;bool&gt;::reference 是一个类类型,一个代表单个bool 的代理。 vector&lt;bool&gt;::operator[] 按值返回这个代理类实例;这同样适用于取消引用 vector&lt;bool&gt;::iterator

vector<bool> cont;
for (auto& e : cont) { ... }

在这里,您尝试将左值引用绑定到右值,这是不允许的。

我应该使用for (cont::reference e : cont)而不是for (auto&amp; e : cont)吗?
诀窍有什么问题?可以增强它以适应任何用例吗?

基于范围的 for 的好处是它也适用于纯 C 数组。使用cont::reference 将失败,以及任何没有名为reference 的成员类型的可迭代类型。如果您想对循环内的容器元素进行只读访问,则应使用for(auto const&amp; e : cont),如果您想修改元素,则应使用for(auto&amp;&amp; e : cont)

在后一种情况下,auto&amp;&amp; e 是一个通用引用,可以绑定到左值和右值,所以它也适用于vector&lt;bool&gt; 的情况。

【讨论】:

  • 这回答了第一个问题(dyp 已经在 cmets 中做到了)。谢谢你的解释。我愿意接受这个答案,因为它使我的 bvect 尝试毫无用处,但仍然......第二个问题呢?我的把戏有什么问题?
  • @firda 后半部分解释了。如果您正在迭代纯 C 数组,则使用 cont::reference 将不起作用,因此它不能扩展到所有用例。
  • 1. typename iterator_traits&lt;decltype(begin(cont))&gt;::reference 甚至适用于 cont=bool[2] 2。我指的是技巧 = bvect::iterator operator *()。 (我看不出答案,但我可以理解我应该改进我的问题)。
  • @firda 1. 你已经从auto&amp;&amp; 变成了可怕的东西,没有明显的好处。 2. 正如在 cmets 中已经提到的 dyp,迭代器要求声明取消引用应该返回 reference 而不是 reference&amp;。忽略这一点,我不会立即看到任何问题,但您的设计似乎不干净。您的 iterator 需要从 reference 继承才能使您的技巧发挥作用。
  • 1.请参阅在我的问题中添加的 FEEDBACK(是的 auto&amp;&amp; 已接受)。 2. 没错,就是这样,看来我违反了标准,但看不出其中的问题。从引用继承迭代器确实是技巧的一部分(但迭代器、指针和引用具有相同或相似的底层结构——因此,在这方面也看不出问题)。
【解决方案2】:

对于空间优化vector&lt;bool&gt; 使用仅存储一位布尔值(而不是像bool 那样的一个字节)。由于operator[] 之类的东西不起作用,您无法以字节为单位获取地址。在您的情况下,您不能在此向量中逐位迭代。你最好使用set&lt;bool&gt;。关于vector&lt;bool&gt;是否应该在STL中有很多讨论。

【讨论】:

  • 我无法迭代?我能!那些范围广泛的 for 循环证明了这一点。我错过了什么? vector::operator[] 返回一个模拟引用的“代理对象”(_Bit_reference)(与我的 bvect::reference 相同)。没有得到你的答案(不明白),对不起。
  • + set 没有意义,它是一组四种可能的状态(空、零、一、两者)。
  • 我认为没有关于vector&lt;bool&gt; 专业化是否应该在标准中的“大量讨论”。我怀疑在 C++98 标准正式发布之前,没有足够的讨论,因为否则共识肯定会是一件坏事。今天,每个人都开始接受这是过早的优化,但由于向后兼容性,删除它是不可接受的。再一次,没有讨论:)
猜你喜欢
  • 1970-01-01
  • 2023-03-13
  • 2015-11-05
  • 2015-01-15
  • 2018-02-17
  • 2019-09-06
  • 1970-01-01
  • 2013-07-21
相关资源
最近更新 更多