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, e.g., 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.,
// Example showing
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:
int k = 0;
int const & j = k; // 'j' is a const reference to 'k'
int const * p = &k; // 'p' is a pointer to const 'k'
const_cast<int const &>(k); // The type of the expression is const
The checking for the correctness of const
is enforced based on the access-path and not the type of the target object. For example, the following are ill-formed as the access path of the left-hand expression is const
-qualified:
0; // int const i;
i = 0; // int const &j
j = 0; // int const *p
*p = const_cast<int const &>(k) = 0; // int k's declaration was not const
Note that the object k
referred to by j
, *p
and the const_cast,
is not constant.
In each case the access path could be changed to remove const
making the program well-formed: const_cast<int&> (j) = 0; // well-formed
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: !!! Needs review re: implied aliasing. Is it undefined behaviour?!!!
#include <cassert>
int volatile k = 0;
void break_it()
{42; // legal
k =
}
void test(int const volatile& j)
{assert(j == 0); // will pass since k == 0
break_it();assert(j == 0); // will fail since k != 0
}
test(k);
We distinguish between qualifications on the pointer’s type (pointer type) and qualifications on the type being referenced (pointee type).
A pointer type can be qualified as const
, however the qualification only applies to the pointer type and not the pointee’s type. A reference type is implicitly immutable, only the referred type can be const
qualified.
using T = int;
using T1 = T &;
using T2 = T *;
using S1 = T1 const; // The const is ignored, S1 has type 'T &'
using S2 = T2 const; // The const applies to the pointer type,
// S2 has type 'T * const'
void foo (S1 s1, S2 s2)
{0; // well-formed
s1 = 0; // well-formed
*s2 = }
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
{// Pointer to non-const A
A * pA; int array[2]; // Array of type int
this}{} // pA provides access path to non-const
A () : pA{// this.
void f () const
{// pA = nullptr; // ill-formed
// array[0] = 0; // ill-formed
0] = 0; // compiles, but undefined behavior
pA->array[// if executed on a const object
} };
In the const member function f
, naming array directly results in a const-qualified access path and so an attempt to modify it is ill-formed. However, the type of pA
is A * const
, that is a const
pointer to a non-const A
. An attempt to modify pA
is ill-formed, however, modification of the value pointed to by pA
is not a const-qualified access path and so is not ill-formed.
The programmer can incorrectly assume that a call to a const member function will not modify the object. However, as has been shown above, there is no guarantee that this is the case. The following example, which follows from the example above, will compile but has undefined behavior as a result of the modification of the const object:
void foo ()
{
A a1 {} ;const a2 {} ;
A // OK - 'a1' is not const
a1.f(); // compiles but has undefined behavior
a2.f(); }
C++ classes wrapping pointer or reference members can be used to provide transitivity of const
within const
member functions. This is shown by the MyRef
type in the following example:
template <typename T>
struct MyRef
{// ...
operator T&() &;
operator T const &() const &;
operator=(T const &) &;
MyRef &
private:
m_t;
T &
};
struct A {
A();
void f1() {
m_i = 0;
m_j = 0;
m_j ++;
m_j;
++
}
void f() const {
m_i = 0; // compiles, but undefined behavior
// if 'm_i' refers to a const object
m_j = 0; // ill-formed
m_j; // ill-formed
++
}
int & m_i;
int> m_j;
MyRef< };
Attempts to modify the object referenced by m_j
are ill-formed when they occur in the const member function f2
.
C++ container iterator types, iterator
and const_iterator
, are examples of use of this pattern.
If a member variable is declared with the mutable
keyword, then it can still be modified, even if the containing object is const
. It is preferable to use mutable
rather than removing the constness of the containing object (see Conversion Errors [FLC]).
Members declared mutable
typically should not contribute to the value of the object. 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
{public:
bool empty () const
{std::lock_guard sg (m_mutex); // lock the mutex, which requires m_mutex to be writable
return m_head != nullptr;
}
// ...
private:
mutable std::mutex m_mutex;
int * 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 the use of cast-away const
.
Leverage the C++ type system to restrict the operations on a type in a way that emulates logical const and document where used.
Be aware that, while const
means “read only”, C++ permits const
values to be modified as shown in clause 6.65.1.
Use const
references or pointers as function arguments (parameters?) as a promise to callers and an obligation to the function declaration and implement this promise. !! NEW !!
Consider the use of constexpr
wherever possible to move the detection of the misuse of const
to compile time.
Apply mutable
only to member variables that do not influence the external behavior of an object.