For testing purposes I created a little unordered_set and tried to iterate over the set. The set holds an own class:
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;
}
...
};
I inserted some elements and tried to change the objects during iteration:
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;
}
}
I used a local_iterator (and not the const_local_iterator) but still I can't change the objects. For some reasons the iterator refers still to a constant object.
My question now: why is this so? If the normal iterator refers to a const object, what is the different between the const and the non-const iterator?
Tested with VisualStudio 2013 and minGW.
Thanks in advance for any help :-)
EDIT: The Hash functor:
struct meinHash {
size_t operator()( const Student& s ) {
return s.getNr();
}
};
For finders of this topic in the future who have the same question, here is some example output if you change the matrNr with violent:
const_cast<Student&>(*iter).setNr( 5 );
and try to display it:
unordered_set<Student, meinHash>::local_iterator iter = meineHashTable.find( 5 );
iter->display();
you may get something like:
Bucketnummer: 0
An empty Bucket
Bucketnummer: 1
Matrikelnummer: 5
Name: Wilma
Bucketnummer: 2
An empty Bucket
Bucketnummer: 3
An empty Bucket
Bucketnummer: 4
Matrikelnummer: 5
Name: Fred
Bucketnummer: 5
An empty Bucket
Bucketnummer: 6
Matrikelnummer: 5
Name: Barney
Bucketnummer: 7
An empty Bucket
//The not wanted output ;-)
Matrikelnummer: -842150451
Name:
Both set
and unordered_set
have read-only keys. It's easy to see why this is the case - if the key value were to change, the data structure would have it filed in the wrong spot and you wouldn't be able to find it anymore.
Per your example, suppose your hash function simply returned the matrNr
field. When the hash number changes, any lookup for 1234
will fail because there's nothing stored in that hash bucket.
It could be possible to change some part of the object that is not used in making the hash key, but that would lead to possible hard to track down bugs. The standards committee decided to eliminate that possibility by making the entire key const.
There are two ways around this restriction. The first is to split the key from the value and use a map
or unordered_map
instead. The second is to remove the item from the set and reinsert it after it's modified.
They value type of a set<K>
is const K
, and for a map<K, T>
it is pair<const K, T>
; ditto for the unordered versions.
An iterator gives you access to value_type &
, and a const-iterator to a const value_type &
. As you can see, neither iterator type can "undo" the constness of the key.
The reason the key is immutable is that it forms an integral part of the underlying data structure; changing the key would require a non-trivial internal rearrangement which would cause all sorts of problems (e.g. non-zero computational complexity (for element access!), and confused iterator ordering).
I had a similar problem and I was confused too. All the sources I looked at indicated that std::unordered_set::find
can return a non-const iterator that dereferences to value_type&
, which is non-const
. On the other hand, all the above answers that state that changing field values within the instance changes its hash and therefore the way it is stored seem to make that impossible. It seems uncharacteristically "sloppy" for the spec to provide an interface that cannot be used, so there has to be a way to do something like what the questioner wants, and there is. You just have to give the compiler enough information to KNOW it's safe to provide you the non-const iterator. To further simplify the original question, we consider the following:
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
}
If you just use the default std::hash<student>
, it folds in all the data from the struct to create the hash - perhaps some combo of std::hash<std::string>(name)
and std::hash<double>(gpa)
. Regardless of how it uses all this data, the compiler behaves as if it incorporates all the data and that's the problem to which the other answers allude, namely that changing any part of record hashed changes its table index. The unordered_set
definition from the original question specifies "MeinHash", but we are not shown what it is, and if it factors in things that might be changed via an iterator, we're back to the problem described by the above answers. Typically though, not all the data in record is used to uniquely id an instance within a set. Let's say "name" is enough to disambiguate the student and gpa is just associated data that we may update. The constructor above strongly implies that, making the call to find
above dangerous. It will create a temp, using the constructor, assign a name and a gpa of 2.0, and then look up the student using BOTH pieces of information. If "edgar" was added to the set with a gpa of 3.0, his record will never be found, let alone updated by the operation on the iterator (which won't even compile). The compiler takes into account the whole lifespan of an iterator when inferring which override of find
to use, so if you use a naive hash function that includes all the fields of the struct, and the compiler sees you changing one of those fields, it "helps" you by failing at compile time. So the first thing you need to do is identify the fields that are truly intrinsic, and required for a hash, and which are not. Then you supply a hash function that uses only these fields - something like the following -
struct student_hash {
std::size_t operator()(const student& hashed_student) {
return std::hash<std::string>()(hashed_student.name);
}
};
For me (using clang), this was not quite enough - necessary, but not sufficient, but at least the compiler now knows that changing "gpa" will have no effect on the index of a record within hash table. I then had to use the mutable
keyword with the declaration of "gpa" to explicitly say to the compiler that this field can change without changing what we writer considers the state of this data. Typically, it's used for refcounts or master pointers and other kinds of meta-data not intrinsic to the state of the struct instance, but it applies here as well. So now we have -
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With