Что из этого следует? К счастью, ничего особенно сложного:
• если переносимость вас не интересует, если вы хотите изменить значение элемента в контейнере
set/multiset
и ваша реализация STL это разрешает — действуйте. Помните о том, что ключевая часть элемента (то есть часть элемента, определяющая порядок сортировки элементов в контейнере) должна сохраниться без изменений;
• если программа должна быть переносимой, элементы контейнеров
set/multiset
модифицироваться не могут (по крайней мере, без преобразования
const_cast
).
Кстати,
о преобразованиях. Вы убедились в том, что изменение вторичных данных элемента
set/multiset
может быть вполне оправданно, поэтому я склонен показать, как это делается — а точнее, делается правильно и переносимо. Сделать это нетрудно, но при этом приходится учитывать тонкость, о которой забывают многие программисты — преобразование должно приводить к ссылке. В качестве примера рассмотрим вызов
setTitle
, который, как было показано, не компилируется в некоторых реализациях:
EmpIDSet::iterator i = se.find(selectedID);
if (i != se.end) {
i->setTitle("Corporate Deity"); // Некоторые реализации STL
} // выдают ошибку в этой строке,
// поскольку *i имеет атрибут const
Чтобы этот фрагмент нормально компилировался и работал, необходимо устранить константность
, и сообщаем компилятору, что результат должен интерпретироваться как ссылка на (неконстантный) объект
Employee
, после чего вызываем
setTitle
для полученной ссылки. Я не буду тратить время на долгие объяснения и лучше покажу, почему альтернативное решение работает совсем не так, как можно было бы ожидать.
Многие программисты пытаются воспользоваться следующим кодом:
if (i != se.end){ // Преобразовать *i
static_cast<Employee>(*i).setTitle("Corporate Deity"); // к Employee
}
Приведенный фрагмент эквивалентен следующему:
if (i != se.end){ // То же самое,
((Employee)(*i)).setTitle("Corporate Deity"); // но с использованием
} // синтаксиса С
Оба фрагмента компилируются, но вследствие эквивалентности работают неправильно. На стадии выполнения объект
*i
не модифицируется, поскольку в обоих случаях результатом преобразования является временный анонимный объект — копия
*i
, и
setTitle
вызывается для анонимного объекта, а не для
*i
! Обе синтаксические формы
эквивалентны следующему фрагменту:
if (i != se.end) {
Employee tempCopy(*i); // Скопировать *i в tempCopy
tempCopy.setTitle("Corporate Deity"); // Изменить tempCopy
}
Становится понятно, почему преобразование должно приводить именно к ссылке — тем самым мы избегаем создания нового объекта. Вместо этого результат преобразования представляет собой ссылку на существующийобъект, на который указывает
i
. При вызове
setTitle
для объекта, обозначенного ссылкой, функция вызывается для
*i
, чего мы и добивались.
Все сказанное хорошо подходит для контейнеров set и multiset, но при переходе к map/multimap ситуация усложняется. Вспомните, что
map<K, V>
и
multimap<K, V>
содержат элементы типа
pair<const K, V>
. Объявление
const
означает, что первый компонент пары определяетсякак константа, а из этого следует, что любые попытки устранить его константность приводят к непредсказуемому результату. Теоретически реализация STL может записывать такие данные в область памяти, доступную только для чтения (например, в страницу виртуальной памяти, которая после исходной записи защищается вызовом системной функции), и попытки устранить их константность в лучшем случае ни к чему не приведут. Я никогда не слышал о реализациях, которые бы поступали подобным образом, но если вы стремитесь придерживаться правил, установленных в Стандарте, — никогдане пытайтесь устранять константность ключей в контейнерах
map
и
multimap
.
Несомненно, вы слышали, что подобные преобразования рискованны. Надеюсь, вы будете избегать их по мере возможности. Выполняя преобразование, вы временно отказываетесь от страховки, обеспечиваемой системой типов, а описанные проблемы дают представление о том, что может произойти при ее отсутствии.
Многие преобразования (включая только что рассмотренные) не являются абсолютно необходимыми. Самый безопасный и универсальный способ модификации элементов контейнера
set
,
multiset
,
map
или
multimap
состоит из пяти простых шагов.
1. Найдите элемент, который требуется изменить. Если вы не уверены в том, как сделать это оптимальным образом, обратитесь к рекомендациям по поводу поиска в совете 45.
2. Создайте копию изменяемого элемента. Помните, что для контейнеров
map/multimap
первый компонент копии не должен объявляться константным — ведь именно его мы и собираемся изменить!
3. Удалите элемент из контейнера. Обычно для этого используется функция
erase
(см. совет 9).
4. Измените копию и присвойте значение, которое должно находиться в контейнере.