CAPÍTULO V: La (edad de) jubilación como momento oficial del inicio de la vejez
2. JUBILARSE MARCA EL MOMENTO CONCRETO DE INICIO DEL ENVEJECIMIENTO
First, a quick review: Here are some key points about what it means to iterate over an associative container, and about what associative containers' iterators do and are.
The four standard associative containers are summarized in Figure 1. Each of these containers internally maintains its elements in a way that allows fast traversal in nondescending key order and fast retrieval by key, where the keys are always compared according to the provided
Compare
type (this defaults toless<Key>
, which uses theoperator<()
provided forKey
objects). Whenever a new key is inserted into a container, the container finds a proper place to insert the new key so that it maintains the proper ordering of its internal data structure.Iterators are easy to implement, because they are supposed to traverse the entries in nondescending
Key
order, and the container already internally stores the elements in a way that naturally supports that ordering. Pretty basic, right?Which brings us to the "key" issue:
The Key Requirement
An essential requirement, without which associative containers could not work reliably at all, is this: Once a key has been inserted into the container, that key had better not be changed in any way that would change its relative position in the container. If that ever did happen, the container wouldn't know about it, and its assumptions about the ordering of its entries would be violated, searches for valid entries could fail, iterators would no longer be guaranteed to traverse the contents in key order, and in general Bad Things would happen.
I'll illustrate this issue in the context of an example, and then discuss what you can do about it.
A Motivating Example
Consider a
map<int,string>
namedm
that has the contents shown in Figure 2; each node withinm
is shown as apair<const int,string>
. I'm showing the internal structure as a binary tree because this is what all current standard library implementations actually use.Figure 2. Sample internal contents of a
map<int, string>
.As the keys are inserted, the tree's structure is maintained and balanced such that a normal inorder traversal visits the keys in the usual
less<int>
ordering. So far, so good.But now say that, through an iterator, we could arbitrarily change the second entry's key, using code that looks something like the following:
1. a) What's wrong with the following code? How would you correct it?
// Example 8-1: Wrong way to change a
// key in a map<int,string> m.
//
map<int,string>::iterator i = m.find( 13 );
if( i != m.end() )
{
const_cast<int&>( i->first ) = 9999999; // oops!
}
Note that we have to cast away
const
to get this code to compile. More on that in a moment. The problem here is that the code interferes with themap
's internal representation by changing themap
's internals in a way that themap
isn't expecting and can't deal with.Example 8-1 corrupts the
map
's internal structure (see Figure 3). Now, for example, an iterator traversal will not return the contents of themap
in key order, as it should. For example, a search for key 144 will probably fail, even though the key exists in themap
. In general, the container is no longer in a consistent or usable state. Note that it is not feasible to require themap
to automatically defend itself against such illicit usage, because it can't even detect this kind of change when it occurs. In Example 8-1, the change was made through a reference into the container, without calling anymap
member functions.
Figure 3. Figure 2 after Example 8-1 executes. Problem!
If the Example 8-1 code had changed the key in such a way that the relative ordering remained unchanged, there would have been no problem for this implementation of
map
. The problem is only with code that attempts to change the relative ordering of keys once they are in the container.What's the best way to deal with this? Ideally, we would like to prevent this through a suitable coding standard. What, then, should such a coding standard say?
Option #1: Say "const Means const!" (Insufficient)
In Example 8-1, we needed to cast away
const
in order to change the key. This is because standard C++ actively tries to prevent code that changes the relative ordering of keys. In fact, standard C++ enforces a (seemingly) stricter requirement—namely, that for bothmap
s andmultimap
s, keys should not be modified at all. Amap<Key, Value>::iterator
points to apair<const
Key, Value>
which, apparently, lets you modify the value part, but not the key part, of the pointed-atmap
entry. This, again apparently, prevents a key from changing at all, much less changing in a way that would alter its position in themap
object's internal ordering.One option, then, is to print large posters that say "
const
meansconst
!" and hold rallies with celebrity speakers and incite mass media coverage to increase awareness of this deplorable problem. This would prevent code like that in Example 8-1, which doesn't work in practice anyway, wouldn't it?It's a good idea, but unfortunately even telling people to respect
const
isn't enough. For example, if theKey
type has amutable
member that affects the wayCompare
comparesKey
objects, then callingconst
member functions on aconst
key can still change the relative ordering of keys.Option #2: Say "Always Change Keys Using Erase-Then-Insert"
(Better, But Still Insufficient)
A better, but still insufficient, solution is to follow this discipline: To change a key, remove it and reinsert it. For example:
b) To what extent are the problems fixed by writing the following instead?
// Example 8-2: Better way to change a key
// in a map<int,string> m.
//
map<int,string>::iterator i = m.find( 13 );
if( i != m.end() )
{
string s = i->second;
m.erase( i );
m.insert( make_pair( 9999999, s ) ); // OK
}
This is better, because it avoids any change to keys, even keys with
mutable
members that are significant in the ordering. It even works with our specific example. So this must be the solution, right? Unfortunately, it's still not enough in the general case, because keys can still be changed while they are in the container. "What?" one might ask. "How can keys be changed while they're in the container, if we adopt the discipline of never changing key objects directly?" Here are two counterexamples:1. Let's say the
Key
type has some externally available state that other code can get at—for example, a pointer to a shared buffer that can be modified by other parts of the system without going through theKey
object. Let's also say that that externally available state participates in the comparison performed byCompare
. Then making a change in the externally available state, even without the knowledge of theKey
object and without the knowledge of the code that uses the associative container, can still change the relative ordering of keys. So in this case, even if the code owning the container tries to follow an erase-then-reinsert discipline, a key ordering change can happen at any time somewhere else and therefore without an erase-then-reinsert operation.2. Consider a