The vulnerability as documented in ISO/IEC TR 24772-1:2019 clause 6.65 exists in C++.
An object can be declared as const, denoting that its value will not change in its lifetime without invoking mechanisms which have undefined behaviour [EWF]. For example, an access path to an object can be declared as const, denoting that the value of the object will not change via this access path without invoking mechanisms which have undefined behaviour, e.g.,
int const i = 0; // the simplest access path
int& j = const_cast<int&>(i); // undefined behaviour
void foo(int* p) { *p += 43; }
//...
foo(const_cast<int*>(&i)); // undefined behaviour
foo(&i); // ill-formed, compiler errorIt is an illegal program or undefined behaviour to attempt to change a const object, such as i, above.
A object that is not const-qualified can be accessed through a path that is const-qualified.
The checking for the correctness of const is enforced based on the access-path and not the type of the target object. While it is possible to remove the const-qualification for an access path, attempting to modify a const object this way is undefined-behavior (see Undefined Behavior [EWF]) : const_cast<int&> (i) = 0; // undefined behavior
A constant can also be legitimately modified via a secondary access path. For example:
#include <cassert>
int k = 0;
void break_it()
{
k = 42; // legal
}
void test(int const volatile& j) // Volatile used only to guarantee observability
{
assert(j == 0); // will pass since k == 0
break_it();
assert(j == 0); // will fail since k != 0
}
test(k);When using pointers, confusion can occur between qualifications on the pointer’s type (pointer type) and qualifications on the type being referenced (pointee type).
A common misconception is that a member function qualified with const cannot modify any of its members.
The following badly defined class introduces a non-const access path to a potentially const object:
struct A
{
A * pA;
int i{0};
A () : pA{this}{} // pA provides non-const access path
void f () const
{
// pA = nullptr; // ill-formed
// i = 0; // ill-formed
pA->i = 42; // compiles, but undefined behavior
// if executed on a const object
}
};
int main(){
A a; // mutable
A const b;
a.f(); // OK
b.f(); // undefined behaviour
}However, for C++ classes with members of a pointer-like type, programmers can establish the transitivity of const with const member functions that do not change the referred-to objects.
Within a const member function a mutable data member can still be modified, even if the containing object is const.
The use of mutable on a data member not contributing to the observable state of the object is preferable to removing the constness of the containing object (see Conversion Errors [FLC]).
Historically, classes with expensive computations in frequently-called const-member-functions, used mutable data members to cache results of those computations. In concurrent code this practice can lead to data races, unless access to those mutable data members is synchronised.
A safe use of mutable data members is for synchronisation primitives, allowing synchronisation in const member functions.
The following is a common example where a mutex member is declared mutable to allow locking in a const member function:
#include <mutex>
class MyQueue {
mutable std::mutex m_mutex;
int * m_head { nullptr };
public:
bool empty () const {
std::lock_guard sg {m_mutex}; // lock the mutex, which requires m_mutex to be writable
return m_head != nullptr;
}
// ...
};To avoid the vulnerability or mitigate its ill effects, C++ software developers can:
Apply the avoidance mechanisms of ISO/IEC 24772-1 clause 6.65.5.
Forbid casting away const by const_cast or by any other means.
Be aware that, while const means “read only”, C++ permits values accessed by a const access path to be modified via a different access path.
Apply const on pointer-like parameters as a promise to callers and an obligation on the implementation to not modify the referred object.
Within const member functions, avoid indirectly modifying the object’s state.
Apply mutable only to member variables that do not influence the observable state of an object.
Ensure in concurrent code, that mutable data members are synchronisation primitives or are explicitly synchronized.
When designing C++ classes, provide const member functions to support const objects.
Ensure that C++ classes modelling pointer-like types provide transitivity of const within const member functions.