Семантика Const-correctness в С++
Для удовольствия и прибыли ™, я пишу класс trie в С++ (используя стандарт С++ 11).
У моего trie<T>
есть итератор, trie<T>::iterator
. (Все они фактически функционально const_iterator
s, потому что вы не можете изменить trie value_type
.) Объявление класса итератора выглядит примерно так:
template<typename T>
class trie<T>::iterator : public std::iterator<std::bidirectional_iterator_tag, T> {
friend class trie<T>;
struct state {
state(const trie<T>* const node, const typename std::vector<std::pair<typename T::value_type, std::unique_ptr<trie<T>>>>::const_iterator& node_map_it ) :
node{node}, node_map_it{node_map_it} {}
// This pointer is to const data:
const trie<T>* node;
typename std::vector<std::pair<typename T::value_type, std::unique_ptr<trie<T>>>>::const_iterator node_map_it;
};
public:
typedef const T value_type;
iterator() =default;
iterator(const trie<T>* node) {
parents.emplace(node, node->children.cbegin());
// ...
}
// ...
private:
std::stack<state> parents;
// ...
};
Обратите внимание, что указатель node
объявлен const
. Это потому, что (на мой взгляд) итератор не должен изменять node, на который он указывает; это просто итератор.
Теперь, в другом месте моего основного класса trie<T>
, у меня есть функция стирания, которая имеет общую подпись STL - для удаления данных требуется iterator
(и возвращает iterator
к следующему объекту).
template<typename T>
typename trie<T>::iterator trie<T>::erase(const_iterator it)
{
// ...
// Cannot modify a const object!
it.parents.top().node->is_leaf = false;
// ...
}
Компилятор жалуется, потому что указатель node
доступен только для чтения! Функция erase
определенно должна изменить trie, на который указывает итератор, хотя итератор не должен.
Итак, у меня есть два вопроса:
- Должны быть публичные конструкторы
iterator
? trie<T>
имеет необходимые члены begin()
и end()
, и, конечно, trie<T>::iterator
и trie<T>
являются общими друзьями, но я не знаю Не знаю, что такое конвенция. Чтобы сделать их частными, я бы решился на то, что у меня возникло желание удалить const
"обещание" из конструктора итератора.
- Каковы правильные семантики/соглашения
const
относительно итератора и его указатель node
здесь? Никто из меня никогда не объяснял это, и я не могу найти никаких учебных пособий или статей о паутина. Вероятно, это более важный вопрос, но для этого требуется много планирования и правильной реализации. Я полагаю, это можно было бы обойти, просто используя 1, но это принцип вещи!
Ответы
Ответ 1
1) Все итераторы должны быть пригодны для копирования. Ваш итератор двунаправлен, поэтому также должен быть конструктивным по умолчанию (http://en.cppreference.com/w/cpp/concept/ForwardIterator), хотя я не знаю, почему. Поэтому конструктор по умолчанию должен быть общедоступным, но вы можете делать то, что вам нравится, с помощью const trie<T>*
. Я бы подумал, что это должно быть конфиденциально, поскольку цель этого класса - предоставить пользователю итератор поверх trie, поэтому его публичный интерфейс должен быть только итератором соответствующей категории. Нет необходимости в каких-либо дополнительных общественных конструкторах.
2) erase
является неконстантной функцией. Единственными итераторами, к которым вы можете действительно перейти, являются итераторы, которые ссылаются на то же самое, что функция вызывается, что означает (я думаю, хотя я не совсем уверен, что я следил за вашим дизайном) вся иерархия родителей неконстантных объектов. Поэтому я подозреваю, что это один из тех случаев, когда вы можете const_cast<trie<T>*>(it.parents.top().node)
. Итератору не разрешено использовать его для изменения trie, поэтому вы хотите, чтобы он удерживал указатель на константу. Но когда вы держите указатель на неконтекст trie, а именно this
, вам разрешено изменять любую его часть, а итератор просто дает вам возможность начать изменять.
Возможно, существует еще один общий принцип состязания безопасности, который вы можете использовать здесь. Один из возможных случаев в container::erase(const_iterator)
- это то, что const container*
, которое вы получили от итератора, равно this
. В этом случае const_cast
, безусловно, является безопасным и законным (а также ненужным, потому что вы можете просто использовать this
, но это касается того, является ли оно const-правильным или нет). В вашем контейнере он не равен (обычно) равным this
, он указывает на один из нескольких объектов trie
, которые вместе составляют иерархический контейнер, частью которого является this
. Хорошей новостью является то, что весь контейнер логически const или логически неконстантен вместе, поэтому const_cast
является столь же безопасным и законным, как если бы это был один объект. Но немного сложнее доказать правильность, потому что вы должны убедиться, что в вашем дизайне весь иерархический контейнер действительно делает, как я предполагал, разделение неконстантности.
Ответ 2
Не <<20 > методы trie
, которые хотят изменить то, что указывает const_iterator
(то, на что он указывает, должно быть в экземпляре this
trie
), должны просто const_cast
по мере необходимости. Вы знаете, что это безопасный и определенный бросок, потому что если кому-то удалось вызвать экземпляр trie
не const
, то этот экземпляр trie
сам не const
. *
В качестве альтернативы вы можете сделать обратное и удерживать указатель < const
<const
<<21 > в пределах const_iterator
, для чего потребуется конструкция const_cast
. Это безопасно по той же причине, выше. Методы const_iterator
предоставляют только const
доступ, поэтому пользователь не может мутировать часть (-ы) trie
, на которую указывает итератор. И если a const_iterator
нужно мутировать в методе не const
trie
, это нормально, потому что trie
не должен был const
в первую очередь. *
Предпосылка безопасности const_cast
здесь заключается в том, что безопасно удерживать и использовать указатель не const
для объекта const
, пока вы не будете мутировать этот объект. И безопасно отключить указатель const
указателя, который указывает на то, что изначально не было объявлено как const
.
* Да, вызывающий метод const
trie
может иметь const_cast
ed в undefined; но в этом случае лордон, вызывающий поведение undefined, находится на их голове, а не trie
s.
Ответ 3
- Должны ли публичные конструкторы
iterator
?
По крайней мере, конструктор копирования, да. Посмотрите на эту диаграмму, которая описывает характеристики, которые должны иметь каждый тип итератора:
http://www.cplusplus.com/reference/iterator/
Все типы итераторов должны быть пригодными для копирования, копировать и уничтожать, поэтому это означает, что им нужны публичные конструкторы копирования. Некоторые итераторы, например RandomAccessIterator
, также должны быть конструктивными по умолчанию, поэтому конструктор по умолчанию должен быть общедоступным.
- Каковы правильные семантики/соглашения
const
относительно итератора и его указателя node
здесь?
Если вы хотите иметь erase
, то у вас действительно нет const_iterator
, у вас есть обычный iterator
. Разница между ними заключается в том, что если у вас есть объект const
trie
, вы можете получить только const_iterator
, потому что вам не разрешено каким-либо образом его изменять.
Вы можете заметить это в контейнерах STL. Они имеют тенденцию иметь:
iterator begin();
const_iterator begin() const;
Обычно происходит реализация const_iterator
:
class iterator : public const_iterator {...};
который реализует одну или две неконстантные функции. Вероятно, это означает только erase
для вас, так как ваш operator*
будет оставаться const.