• No se han encontrado resultados

JUBILARSE MARCA EL MOMENTO CONCRETO DE INICIO DEL ENVEJECIMIENTO

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 to

less<Key>

, which uses the

operator<()

provided for

Key

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>

named

m

that has the contents shown in Figure 2; each node within

m

is shown as a

pair<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 the

map

's internal representation by changing the

map

's internals in a way that the

map

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 the

map

in key order, as it should. For example, a search for key 144 will probably fail, even though the key exists in the

map

. In general, the container is no longer in a consistent or usable state. Note that it is not feasible to require the

map

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 any

map

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 both

map

s and

multimap

s, keys should not be modified at all. A

map<Key, Value>::iterator

points to a

pair<const

Key, Value>

which, apparently, lets you modify the value part, but not the key part, of the pointed-at

map

entry. This, again apparently, prevents a key from changing at all, much less changing in a way that would alter its position in the

map

object's internal ordering.

One option, then, is to print large posters that say "

const

means

const

!" 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 the

Key

type has a

mutable

member that affects the way

Compare

compares

Key

objects, then calling

const

member functions on a

const

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 the

Key

object. Let's also say that that externally available state participates in the comparison performed by

Compare

. Then making a change in the externally available state, even without the knowledge of the

Key

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

Key

type of

string

and a

Compare

type that interprets the key as a file name and compares the contents of the files. It's obvious that even if the keys are never changed, the relative ordering of keys can still change if the files are modified by another process, or (if the file is shared on a network) even by a user on a different machine on the other side of the world.