For fun and profit™, I'm writing a trie class in C++ (using the C++11 standard.)
My trie<T>
has an iterator, trie<T>::iterator
. (They're all actually functionally const_iterator
s, because you cannot modify a trie's value_type
.) The iterator's class declaration looks partially like this:
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;
// ...
};
Notice that the node
pointer is declared const
. This is because (in my mind) the iterator should not be modifying the node that it points to; it is just an iterator.
Now, elsewhere in my main trie<T>
class, I have an erase function that has a common STL signature--it takes an iterator
to data to erase (and returns an iterator
to the next object).
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;
// ...
}
The compiler complains because the node
pointer is read-only! The erase
function definitely should modify the trie that the iterator points to, even though the iterator shouldn't.
So, I have two questions:
iterator
's constructors be public? trie<T>
has the necessary begin()
and end()
members, and of course trie<T>::iterator
and trie<T>
are mutual friends, but I don't know what the convention is. Making them private would solve a lot of the angst I'm having about removing the const
"promise" from the iterator's constructor.const
semantics/conventions regarding the iterator and its node
pointer here? Nobody has ever explained this to me, and I can't find any tutorials or articles on the Web. This is probably the more important question, but it does require a good deal of planning and proper implementation. I suppose it could be circumvented by just implementing 1, but it's the principle of the thing!In C, C++, and D, all data types, including those defined by the user, can be declared const , and const-correctness dictates that all variables or objects should be declared as such unless they need to be modified.
Declaring function parameters const indicates that the function promises not to change these values. In C, function arguments are passed by value rather than by reference. Although a function may change the values passed in, these changed values are discarded once the function returns.
The following is an example of a simple declaration of a constant integer: const int number = 42; Note that the const in the above declaration can also come after the type, as in the following: int const number = 42; In a simple const variable declaration, any storage type may be used.
In general, const is a better option if we have a choice and it can successfully apply to the code. There are situations when #define cannot be replaced by const. For example, #define can take parameters (See this for example). #define can also be used to replace some text in a program with another text.
1) All iterators are required to be copy-constructible. Your iterator is Bi-directional, hence is also required to be default-constructible (http://en.cppreference.com/w/cpp/concept/ForwardIterator), although I don't know why. So the default constructor needs to be public but you can do what you like with the const trie<T>*
one. I would think it should be private, since the purpose of this class is to provide the user with an iterator over the trie, and so its public interface should be only that of an iterator of the appropriate category. No need for any extra public constructors.
2) erase
is a non-const function. The only iterators you can validly pass to it are iterators that refer to the same trie that the function is called on, which means (I think, although I'm not quite certain I've followed your design) the whole hierarchy of parents are non-const objects. So I suspect this is one of those cases where you can const_cast<trie<T>*>(it.parents.top().node)
. The iterator isn't allowed to use it to modify the trie, which is why you want it to hold a pointer-to-const. But when you hold a non-const pointer to the trie, namely this
, you are allowed to modify any part of it you like, and the iterator is just giving you the position to start modifying from.
There might be some more general principle of const-safety you can draw here. One possible case in container::erase(const_iterator)
functions is that the const container*
you'd get from the iterator is equal to this
. In that case the const_cast
is certainly both safe and legitimate (as well as unnecessary, because you can just use this
, but that's beside the point of whether it's const-correct or not). In your container it is not (in general) equal to this
, it points to one of the several trie
objects that together make up the hierarchical container that this
is part of. The good news is, that whole container is logically const or logically non-const together, hence the const_cast
is just as safe and legitimate as if it were all one object. But a bit harder to prove correct, because you have to make sure that in your design the whole hierarchical container genuinely does, as I've assumed, share non-constness.
Non-const
methods of trie
that wish to modify what a const_iterator
points to (what it points to should be within this
instance of trie
) should just const_cast
as needed. You know this is a safe and defined cast because if someone managed to call a non-const
instance of this trie
, then this instance of trie
itself is not const
.*
Alternatively you could do the opposite and hold non-const
pointer(s) to the trie
within the const_iterator
, which will require a const_cast
upon construction. This is safe for the same reason, above. The const_iterator
methods will only provide const
access, so the user can't mutate the part(s) of trie
that the iterator points to. And if a const_iterator
needs to be mutated in a non-const
trie
method it's okay because the trie
must not have been const
in the first place.*
The premise of the safety of const_cast
ing here is that it is safe to hold and use a non-const
pointer to a const
object so long as you do not mutate that object. And it is safe to turn a cast away the const
ness of a pointer which points to something which wasn't originally declared as const
.
*Yes, the caller of the non-const
trie
method might have const_cast
ed in an undefined way; but in that case the burdon of causing undefined behavior is on their head, not trie
s.
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