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, 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; }
//...
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:

  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:

  i = 0; // int const i;
  j = 0; // int const &j
  *p = 0; // int const *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() 
{
  k = 42; // legal
}

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)
  {
    s1 = 0;            // well-formed
    *s2 = 0;           // well-formed
  }

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;                // Pointer to non-const A
    int array[2];            // Array of type int

    A () : pA{this}{}      // pA provides access path to non-const
                           // this.

    void f () const
    {
      // pA = nullptr;     // ill-formed
      // array[0] = 0;     // ill-formed

      pA->array[0] = 0;    // compiles, but undefined behavior
                           // 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 {} ;
     A const a2 {} ;
     a1.f();           // OK - 'a1' is not const
     a2.f();           // compiles but has undefined behavior
  }

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 &;
  MyRef & operator=(T const &) &;

private:
  T & m_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;
  MyRef<int> m_j;
};

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 };
};

6.65.2 Avoidance mechanisms for language users

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