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