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; }
//...
const_cast<int*>(&i)); // undefined behaviour
foo(// ill-formed, compiler error foo(&i);
It 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()
{42; // legal
k =
}
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};
this}{} // pA provides non-const access path
A () : pA{
void f () const
{// pA = nullptr; // ill-formed
// i = 0; // ill-formed
42; // compiles, but undefined behavior
pA->i = // if executed on a const object
}
};
int main(){
// mutable
A a; const b;
A // OK
a.f(); // undefined behaviour
b.f(); }
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:
Use 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.
Use 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.