Are std::vector Elements Logically or Physically Const in C++?
Understanding Constness: Physical vs. Logical
In C++, understanding the difference between physical (bitwise) constness and logical constness is crucial when writing low-level or high-performance code.
- Physical/Bitwise Constness: An object is physically const if its actual memory resides in read-only storage or if the compiler treats the object's direct memory bytes as immutable. Modifying a physically const object yields Undefined Behavior (UB).
- Logical Constness: An object is logically const if its public interface prevents modification to maintain invariants, even though the underlying state or dynamically allocated memory can technically be modified without violating language-level memory safety rules.
How std::vector Works Under the Hood
To understand the constness of std::vector<T> elements, we must inspect how a vector stores its data. A std::vector typically consists of three pointers pointing to dynamically allocated memory on the heap:
template <typename T>
class vector {
T* begin_;
T* end_;
T* capacity_end_;
// ...
};When you instantiate a const std::vector<int> v{1, 2, 3};, the vector object itself is marked const. This means its internal member pointers become T* const (a constant pointer to non-constant data), not const T* (a pointer to constant data).
Are Vector Elements Logically or Physically Const?
The elements of a const std::vector<T> are logically const, not physically const.
Because the elements are allocated dynamically on the heap via an allocator (e.g., std::allocator<T>), the elements themselves were initialized as non-const objects of type T. The vector's const access operator operator[] const returns a const T& to enforce logical constness through the class interface, but the heap objects themselves do not possess physical/bitwise constness.
Is Using const_cast Undefined Behavior Here?
Consider the following snippet:
const std::vector<int> v{1, 2, 3};
const_cast<int&>(v[0]) = 42; // Is this Undefined Behavior?According to the C++ standard ([dcl.type.cv]):
Attempting to modify a const object during its lifetime results in undefined behavior.
The critical distinction here is: What is the actual object?
- The vector object
vitself is declaredconst. Modifyingv's internal pointer members (likebegin_) viaconst_castwould indeed cause Undefined Behavior. - The vector element
v[0]lives on the heap. It was created as a non-constintobject during allocation/construction.
Because the element on the heap was never declared as a const object, casting away the reference's constness and assigning a value to it is well-defined behavior on a language level.
Summary & Best Practices
- Elements of a
const std::vector<T>are logically const because they reside in dynamically allocated memory created as non-const objects. - Modifying an element of a
const std::vectorusingconst_castis legal and defined by the C++ standard, provided the underlying element was not originally instantiated as aconstobject. - Warning: While technically well-defined behavior, doing this is bad practice as it violates encapsulation and breaks class invariants. Avoid using
const_castin production code unless interfacing with legacy non-const C-APIs.