C++ this Pointer CV-Qualifiers


Example

this can also be cv-qualified, the same as any other pointer. However, due to the this parameter not being listed in the parameter list, special syntax is required for this; the cv-qualifiers are listed after the parameter list, but before the function's body.

struct ThisCVQ {
    void no_qualifier()                {} // "this" is: ThisCVQ*
    void  c_qualifier() const          {} // "this" is: const ThisCVQ*
    void  v_qualifier() volatile       {} // "this" is: volatile ThisCVQ*
    void cv_qualifier() const volatile {} // "this" is: const volatile ThisCVQ*
};

As this is a parameter, a function can be overloaded based on its this cv-qualifier(s).

struct CVOverload {
    int func()                { return    3; }
    int func() const          { return   33; }
    int func() volatile       { return  333; }
    int func() const volatile { return 3333; }
};

When this is const (including const volatile), the function is unable to write to member variables through it, whether implicitly or explicitly. The sole exception to this is mutable member variables, which can be written regardless of const-ness. Due to this, const is used to indicate that the member function doesn't change the object's logical state (the way the object appears to the outside world), even if it does modify the physical state (the way the object looks under the hood).

Logical state is the way the object appears to outside observers. It isn't directly tied to physical state, and indeed, might not even be stored as physical state. As long as outside observers can't see any changes, the logical state is constant, even if you flip every single bit in the object.

Physical state, also known as bitwise state, is how the object is stored in memory. This is the object's nitty-gritty, the raw 1s and 0s that make up its data. An object is only physically constant if its representation in memory never changes.

Note that C++ bases constness on logical state, not physical state.

class DoSomethingComplexAndOrExpensive {
    mutable ResultType cached_result;
    mutable bool state_changed;

    ResultType calculate_result();
    void modify_somehow(const Param& p);

    // ...

  public:
    DoSomethingComplexAndOrExpensive(Param p) : state_changed(true) {
        modify_somehow(p);
    }

    void change_state(Param p) {
        modify_somehow(p);
        state_changed = true;
    }

    // Return some complex and/or expensive-to-calculate result.
    // As this has no reason to modify logical state, it is marked as "const".
    ResultType get_result() const;
};
ResultType DoSomethingComplexAndOrExpensive::get_result() const {
    // cached_result and state_changed can be modified, even with a const "this" pointer.
    // Even though the function doesn't modify logical state, it does modify physical state
    //  by caching the result, so it doesn't need to be recalculated every time the function
    //  is called.  This is indicated by cached_result and state_changed being mutable.

    if (state_changed) {
        cached_result = calculate_result();
        state_changed = false;
    }

    return cached_result;
}

Note that while you technically could use const_cast on this to make it non-cv-qualified, you really, REALLY shouldn't, and should use mutable instead. A const_cast is liable to invoke undefined behaviour when used on an object that actually is const, while mutable is designed to be safe to use. It is, however, possible that you may run into this in extremely old code.

An exception to this rule is defining non-cv-qualified accessors in terms of const accessors; as the object is guaranteed to not be const if the non-cv-qualified version is called, there's no risk of UB.

class CVAccessor {
    int arr[5];

  public:
    const int& get_arr_element(size_t i) const { return arr[i]; }

    int& get_arr_element(size_t i) {
        return const_cast<int&>(const_cast<const CVAccessor*>(this)->get_arr_element(i));
    }
};

This prevents unnecessary duplication of code.


As with regular pointers, if this is volatile (including const volatile), it is loaded from memory each time it is accessed, instead of being cached. This has the same effects on optimisation as declaring any other pointer volatile would, so care should be taken.


Note that if an instance is cv-qualified, the only member functions it is allowed to access are member functions whose this pointer is at least as cv-qualified as the instance itself:

  • Non-cv instances can access any member functions.
  • const instances can access const and const volatile functions.
  • volatile instances can access volatile and const volatile functions.
  • const volatile instances can access const volatile functions.

This is one of the key tenets of const correctness.

struct CVAccess {
    void    func()                {}
    void  func_c() const          {}
    void  func_v() volatile       {}
    void func_cv() const volatile {}
};

CVAccess cva;
cva.func();    // Good.
cva.func_c();  // Good.
cva.func_v();  // Good.
cva.func_cv(); // Good.

const CVAccess c_cva;
c_cva.func();    // Error.
c_cva.func_c();  // Good.
c_cva.func_v();  // Error.
c_cva.func_cv(); // Good.

volatile CVAccess v_cva;
v_cva.func();    // Error.
v_cva.func_c();  // Error.
v_cva.func_v();  // Good.
v_cva.func_cv(); // Good.

const volatile CVAccess cv_cva;
cv_cva.func();    // Error.
cv_cva.func_c();  // Error.
cv_cva.func_v();  // Error.
cv_cva.func_cv(); // Good.