Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Const-correctness semantics in C++

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_iterators, 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:

  1. Should 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.
  2. What are the correct 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!
like image 264
George Hilliard Avatar asked Oct 22 '13 20:10

George Hilliard


People also ask

What is const correctness in C?

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.

What is const argument in C?

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.

What is const keyword in C with example?

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.

Is const better than #define in C?

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.


2 Answers

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.

like image 134
Steve Jessop Avatar answered Oct 08 '22 19:10

Steve Jessop


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_casting 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 constness 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_casted in an undefined way; but in that case the burdon of causing undefined behavior is on their head, not tries.

like image 41
David Avatar answered Oct 08 '22 19:10

David