C++ Member Function cv-qualifier Overloading


Example

Functions within a class can be overloaded for when they are accessed through a cv-qualified reference to that class; this is most commonly used to overload for const, but can be used to overload for volatile and const volatile, too. This is because all non-static member functions take this as a hidden parameter, which the cv-qualifiers are applied to. This is most commonly used to overload for const, but can also be used for volatile and const volatile.

This is necessary because a member function can only be called if it is at least as cv-qualified as the instance it's called on. While a non-const instance can call both const and non-const members, a const instance can only call const members. This allows a function to have different behaviour depending on the calling instance's cv-qualifiers, and allows the programmer to disallow functions for an undesired cv-qualifier(s) by not providing a version with that qualifier(s).

A class with some basic print method could be const overloaded like so:

#include <iostream>

class Integer
{
    public:
        Integer(int i_): i{i_}{}

        void print()
        {
            std::cout << "int: " << i << std::endl;
        }

        void print() const
        {
            std::cout << "const int: " << i << std::endl;
        }

    protected:
        int i;
};

int main()
{
    Integer i{5};
    const Integer &ic = i;
    
    i.print(); // prints "int: 5"
    ic.print(); // prints "const int: 5"
}

This is a key tenet of const correctness: By marking member functions as const, they are allowed to be called on const instances, which in turn allows functions to take instances as const pointers/references if they don't need to modify them. This allows code to specify whether it modifies state by taking unmodified parameters as const and modified parameters without cv-qualifiers, making code both safer and more readable.

class ConstCorrect 
{
  public:
    void good_func() const 
    {
        std::cout << "I care not whether the instance is const." << std::endl;
    }

    void bad_func() 
    {
        std::cout << "I can only be called on non-const, non-volatile instances." << std::endl;
    }
};

void i_change_no_state(const ConstCorrect& cc) 
{
    std::cout << "I can take either a const or a non-const ConstCorrect." << std::endl;
    cc.good_func(); // Good.  Can be called from const or non-const instance.
    cc.bad_func();  // Error.  Can only be called from non-const instance.
}

void const_incorrect_func(ConstCorrect& cc) 
{
    cc.good_func(); // Good.  Can be called from const or non-const instance.
    cc.bad_func();  // Good.  Can only be called from non-const instance.
}

A common usage of this is declaring accessors as const, and mutators as non-const.


No class members can be modified within a const member function. If there is some member that you really need to modify, such as locking a std::mutex, you can declare it as mutable:

class Integer
{
    public:
        Integer(int i_): i{i_}{}

        int get() const
        {
            std::lock_guard<std::mutex> lock{mut};
            return i;
        }

        void set(int i_)
        {
            std::lock_guard<std::mutex> lock{mut};
            i = i_;
        }

    protected:
        int i;
        mutable std::mutex mut;
};