parts/6.65.ModifyingConstants-UJO.md

6.65 Modifying constants [UJO]

6.65.1 Applicability to language

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 error

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() 
{
  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;
  }

  // ...

};

6.65.2 Avoidance mechanisms for language users

To avoid the vulnerability or mitigate its ill effects, C++ software developers can: