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.