const vs non-const of container and its content

C++ only supports one level of const. As far as the compiler is concerned, it is bitwise const: the "bits" actually in the object (i.e. counted in sizeof) cannot be modified without playing games (const_cast, etc.), but anything else is fair game. In the early days of C++ (late 1980s, early 1990s) there was a lot of discussion as to the design advantages of bitwise const vs. logical const (also known as Humpty-Dumpty const, because as Andy Koenig once told me, when the programmer uses const, it means exactly what the programmer wants it to mean). The consensus finally coalesced in favor of logical const.

This does mean that authors of container classes have to make a choice. Are the elements of the container part of the container, or not. If they're part of the container, then they cannot be modified if the container is const. There's no way to offer a choice; the author of the container has to choose one or the other. Here too, there seems to be a consensus: the elements are part of the container, and if the container is const, they cannot be modified. (Perhaps the parallel with C style arrays played a role here; if a C style array is const, then you cannot modify any of its elements.)

Like you, I've encountered times when I wanted to forbid modification of the size of the vector (perhaps to protect iterators), but not of its elements. There are no really satisfactory solutions; the best I can think of is to create a new type, which contains a mutable std::vector, and provide forwarding functions which correspond to the meaning of const I need in this specific case. And if you want to distinguish three levels (completely const, partially const, and non-const), you'll need derivation. The base class only exposes the completely const and partially const functions (e.g. a const int operator[]( size_t index ) const; and int operator[]( size_t index );, but not void push_back( int );); the functions which allow insertion and removal of an element are only exposed in the derived class. Clients which shouldn't insert or remove elements are only passed a non-const reference to the base class.


Unfortunately, unlike pointers, you can't do something like

std::vector<int> i;
std::vector<const int>& ref = i;

That's why std::vector can't disambiguate between the two kinds of const as they might apply, and it has to be conservative. I, personally, would choose to do something like

const_cast<int&>(X[i]);

Edit: As another commenter accurately pointed out, iterators do model this dichotomy. If you stored a vector<int>::iterator to the beginning, you could then de-reference it in a const method and get back a non-const int&. I think. But you'd have to be careful of invalidation.


It's not a strange design, it's a very deliberate choice, and the right one IMHO.

Your B example is not a good analogy for a std::vector, a better analogy would be:

struct C {
   int& get(int i) const { return X[i]; }
   int X[N];
};

but with the very useful difference that the array can be resized. The code above is invalid for the same reason as your original is, the array (or vector) elements are conceptually "members" (technically sub-objects) of the containing type, so you should not be able to modify them through a const member function.

I would say the const_cast is not acceptable, and neither is using mutable unless as a last resort. You should ask why you want to change a const object's data, and consider making the member function non-const.