【问题标题】:unordered_set non const iteratorunordered_set 非常量迭代器
【发布时间】:2013-09-13 06:57:46
【问题描述】:

出于测试目的,我创建了一个小 unordered_set 并尝试迭代该集合。该集合拥有一个自己的类:

class Student {
private:
    int matrNr;
    string name;
public:
    Student( const int& matrNr = 0, const string& name = "" )
        : matrNr( matrNr ), name( name ) {}
    void setNr( const int& matrNr ) {
        this->matrNr = matrNr;
    }
...
};

我插入了一些元素并尝试在迭代过程中更改对象:

unordered_set<Student, meinHash> meineHashTable;
meineHashTable.emplace( 12, "Fred" );
meineHashTable.emplace( 22, "Barney" );
meineHashTable.emplace( 33, "Wilma" );

for (int i = 0; i < meineHashTable.bucket_count(); i++) {
    cout << "Bucketnummer: " << i << endl;
    unordered_set<Student, meinHash>::local_iterator iter;  // not constant?!?

    if (meineHashTable.bucket_size( i ) > 0) {
        for (iter = meineHashTable.begin( i ); iter != meineHashTable.end( i ); iter++) {
            //const_cast<Student&>(*iter).setNr( 1234 );  //This does work
            iter->setNr( 1234 );  //This does not work
        }

    }
    else {
        cout << "An empty Bucket" << endl;
    }

}

我使用了 local_iterator(而不是 const_local_iterator),但我仍然无法更改对象。由于某些原因,迭代器仍然指向一个常量对象。

我现在的问题是:为什么会这样?如果普通迭代器引用的是一个 const 对象,那么 const 和非 const 迭代器有什么区别?

使用 VisualStudio 2013 和 minGW 测试。

在此先感谢您的帮助 :-)

编辑: 哈希函子:

struct meinHash {
    size_t operator()( const Student& s ) {
        return s.getNr();
    }
};

对于以后有相同问题的该主题的发现者,如果您将matrNr更改为暴力,以下是一些示例输出:

const_cast<Student&>(*iter).setNr( 5 );

并尝试显示它:

unordered_set<Student, meinHash>::local_iterator iter = meineHashTable.find( 5 );
iter->display();

你可能会得到类似的东西:

桶数:0

一个空桶

桶数:1

Matrikelnummer:5

姓名:威尔玛

桶数:2

一个空桶

桶数:3

一个空桶

桶数:4

Matrikelnummer:5

姓名:弗雷德

桶数:5

一个空桶

桶数:6

Matrikelnummer:5

姓名:巴尼

桶数:7

一个空桶

//不需要的输出 ;-)

Matrikelnummer:-842150451

姓名:

【问题讨论】:

  • 当“这不起作用”时它是如何失败的?编译时间还是运行时间?错误信息?

标签: c++ c++11 stl unordered-set


【解决方案1】:

setunordered_set 都有只读密钥。很容易看出为什么会这样 - 如果键值发生变化,数据结构会将其归档到错误的位置,您将无法再找到它。

根据您的示例,假设您的哈希函数仅返回 matrNr 字段。当哈希值发生变化时,对1234 的任何查找都将失败,因为该哈希桶中没有存储任何内容。

可以更改对象中未用于生成哈希键的部分,但这可能会导致难以追踪错误。标准委员会决定通过使整个关键 const 消除这种可能性。

有两种方法可以绕过这个限制。第一种是从值中拆分键并使用mapunordered_map 代替。第二种是从集合中移除项目,修改后重新插入。

【讨论】:

  • 您猜对了,matrNr 用于散列;-)。我想我理解这个问题。如果我在迭代期间将所有对象的 matrNr 更改为 5(使用 const_cast)并在之后尝试找到它,由于之前存储桶编号 5 是空的,我会导致未定义的行为。
【解决方案2】:

set&lt;K&gt; 的值类型是 const Kmap&lt;K, T&gt; 的值类型是 pair&lt;const K, T&gt;;无序版本也是如此。

迭代器使您可以访问value_type &amp;,而常量迭代器使您可以访问const value_type &amp;。如您所见,两种迭代器类型都不能“撤消”键的常量性。

密钥不可变的原因是它构成了底层数据结构的一个组成部分;更改密钥需要进行重要的内部重新排列,这会导致各种问题(例如,非零计算复杂度(用于元素访问!)和混淆的迭代器排序)。

【讨论】:

  • 呃,即使你改变了顺序,迭代器也不会失效(它们基本上是指向节点的指针)。
  • @Xeo:我的意思是迭代顺序会变得混乱,如果你在循环中迭代,如果你明白我的意思。
【解决方案3】:

unordered_set 是一种数据结构,您无法在不更改其位置的情况下修改项目。

这里的非 const 迭代器是 const ,因为 STL 确实可以保护您免受如此明显的错误。

如果您想修改 unordered_set 的项目,您必须将其删除并重新添加。

【讨论】:

    【解决方案4】:

    我有类似的问题,我也很困惑。我查看的所有来源都表明std::unordered_set::find 可以返回一个非常量迭代器,该迭代器取消引用value_type&amp;,它是非const。另一方面,上述所有答案都表明更改实例中的字段值会更改其哈希值,因此它的存储方式似乎使这成为不可能。规范提供一个无法使用的接口似乎一反常态地“草率”,所以必须有一种方法来做一些提问者想要的事情,而且确实存在。您只需向编译器提供足够的信息,就知道为您提供非常量迭代器是安全的。为了进一步简化原始问题,我们考虑以下几点:

    struct student {
      std::string name;
      double gpa;
    
      // necessary for a decent member of a hash table. Compares all fields by default
      bool operator==(const student& other) const = default;
    
      student(const char* _name)
        : name(_name)
        , gpa(2.0) {}
    };
    
    std::unordered_set<student> student_set;
    
    auto found = student_set.find("edgar"); // danger!! See note below
    if (found != student_set.end()) {
      found->gpa = 4.0;  // <- compile failure here. "found" is of type const_iterator
    }
    

    如果您只使用默认的std::hash&lt;student&gt;,它会将结构中的所有数据折叠起来以创建散列 - 可能是std::hash&lt;std::string&gt;(name)std::hash&lt;double&gt;(gpa) 的组合。无论它如何使用所有这些数据,编译器的行为都好像它合并了所有数据,这就是其他答案所暗示的问题,即更改记录散列的任何部分都会更改其表索引。原始问题中的unordered_set 定义指定了“MeinHash”,但我们没有看到它是什么,如果它考虑到可能通过迭代器改变的事情,我们就会回到上述答案所描述的问题。但是,通常情况下,并非记录中的所有数据都用于唯一标识集合中的实例。假设“姓名”足以消除学生的歧义,而 gpa 只是我们可能更新的关联数据。上面的构造函数强烈暗示,对上面的find 的调用很危险。它将使用构造函数创建一个临时,分配一个名称和 2.0 的 gpa,然后使用这两条信息查找学生。如果将“edgar”添加到 gpa 为 3.0 的集合中,他的记录将永远找不到,更不用说通过迭代器上的操作更新(甚至不会编译)。在推断要使用 find 的哪个覆盖时,编译器会考虑迭代器的整个生命周期,因此,如果您使用包含结构的所有字段的简单哈希函数,并且编译器会看到您更改其中一个字段,它通过在编译时失败来“帮助”你。因此,您需要做的第一件事是识别真正内在的、散列所需的字段,哪些不是。然后你提供一个只使用这些字段的哈希函数 - 如下所示 -

    struct student_hash {
       std::size_t operator()(const student& hashed_student) {
         return std::hash<std::string>()(hashed_student.name);
      }
    };
    

    对我(使用 clang)来说,这还不够——必要,但还不够,但至少编译器现在知道更改“gpa”不会影响哈希表中记录的索引。然后我不得不使用带有“gpa”声明的mutable 关键字来明确告诉编译器这个字段可以改变而不改变我们认为这个数据的状态。通常,它用于引用计数或主指针和其他类型的元数据,这些元数据不是结构实例状态所固有的,但它也适用于此。所以现在我们有 -

    struct student {
      std::string name;
      mutable double gpa;
    
      // indicates that a matching name means a hit
      bool operator==(const student& other) const {
        return name.compare(other.name) == 0;
      }
    
      student(const char* _name)
        : name(_name)
        , gpa(2.0) {}
    };
    
    std::unordered_set<student, student_hash> student_set;
    
    auto found = student_set.find("edgar"); // will find "edgar" regardless of gpa
    if (found != student_set.end()) {
      found->gpa = 4.0;  // <- no longer fails here. "found" is of type iterator
    }
    

    【讨论】:

    • 我进行了很多编辑,因为我的用例有一些必要的东西,以及在我的配对案例中必要的其他原因(因此,我不知道它们是必要的)。即operator==。我能说什么?我不是编译器。
    【解决方案5】:

    您可以将 const 类型转换为非 const 类型。通过这种方式,您“告诉编译器”您知道自己在做什么,因此您确实应该知道自己在做什么。

    【讨论】:

    • 迭代器无法编辑且不是 const_iterator 的问题
    猜你喜欢
    • 2017-11-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多