In lesson 4.13 -- Const variables and symbolic constants, you learned that objects of a fundamental data type (int
, double
, char
, etc…) can be made constant via the const
keyword. All const variables must be initialized at time of creation.
const int x; // compile error: not initialized
const int y{}; // ok: value initialized
const int z{ 5 }; // ok: list initialized
Similarly, class objects can also be made const by using the const
keyword. Such objects must also be initialized at the time of creation.
// assume Date is a class
const Date date1; // ok: default initialized using default constructor
const Date date2{}; // ok: value initialized using default constructor
const Date date3{ 2020, 10, 16 }; // ok: initialized using parameterized constructor
Just like with normal variables, you’ll generally want to make your class objects const when you need to ensure they aren’t modified after creation.
Modifying the data members of const objects is disallowed
Once a const class type object has been initialized, any attempt to modify the data members of the object is disallowed, as it would violate the const-ness of the object. This includes both changing member variables directly (if they are public), or calling member functions that set the value of member variables. Consider the following class:
class Something
{
public:
int m_value{}; // public just for sake of example
void setValue(int value) { m_value = value; }
int getValue() { return m_value ; }
};
int main()
{
const Something something{}; // something is const
something.m_value = 5; // compile error: violates const
something.setValue(5); // compile error: violates const
return 0;
}
Both of the above lines involving variable something are illegal because they violate the constness of something
by either attempting to change a member variable directly, or by calling a member function that attempts to change a member variable.
Const objects may not call non-const member functions
You may be surprised to find that this code also causes a compilation error:
#include <iostream>
class Something
{
private:
int m_value{};
public:
int getValue() { return m_value ; } // does not modify m_value
};
int main()
{
const Something something{}; // something is const
std::cout << something.getValue(); // compile error: violates const
return 0;
}
Even though getValue()
does not try to modify a member variable, our call to something.getValue()
is still a const violation. This happens because the getValue()
member function is not declared as const. The compiler won’t let us call a non-const member function on a const object.
Const member functions
To address the above issue, we need to make getValue()
a const member function. A const member function is a member function that guarantees it will not modify the object or call any non-const member functions (as they may modify the object).
Making getValue()
a const member function is easy -- we simply append the const
keyword to the function prototype, after the parameter list, but before the function body:
#include <iostream>
class Something
{
private:
int m_value{};
public:
int getValue() const { return m_value ; } // now a const member function
};
int main()
{
const Something something{}; // something is const
std::cout << something.getValue(); // ok to call const member function on const object
return 0;
}
In the above example, getValue()
has been made a const member function, which means we can call it on const objects.
For member functions defined outside of the class definition, the const
keyword must be used on both the function prototype in the class definition and on the function definition:
#include <iostream>
class Something
{
private:
int m_value{};
public:
int getValue() const; // declaration is const
};
int Something::getValue() const // definition is also const
{
return m_value;
}
int main()
{
const Something something{};
std::cout << something.getValue();
return 0;
}
A const member function that attempts to change a member variable or call a non-const member function will cause a compiler error to occur. For example:
class Something
{
private:
int m_value{};
public:
void resetValue() const { m_value = 0; } // compile error: const function modifies member
};
int main()
{
return 0;
}
In this example, resetValue()
has been marked as a const member function, but it attempts to change m_value
. This will cause a compiler error.
Note that constructors cannot be made const. This is because constructors need to be able to initialize their member variables, and a const constructor would not be able to do so. Consequently, the language disallows const constructors.
Const member functions may be called on non-const objects
Const member functions can also be called on non-const objects:
#include <iostream>
class Something
{
private:
int m_value{};
public:
int getValue() const { return m_value ; } // const member function
};
int main()
{
Something something{}; // something is non-const
std::cout << something.getValue(); // ok to call const member function on non-const object
return 0;
}
Because const member functions can be called on both const and non-const objects, if a member function does not modify the state of the object, it should be made const.
Best practice
A member function that does not (and will not ever) modify the state of the object should be made const, so that it can be called on const objects.
Once a member function is made const, that const is part of the interface of the class. Later removal of const
on a member function will break any code that calls that member function on a const object. Therefore, this should be avoided.
Const objects via pass by const reference
Although instantiating locally scoped const class objects is one way to create const objects, a more common way to get a const object is by passing an object to a function by const reference.
In lesson 12.5 -- Pass by lvalue reference, we covered the merits of passing class arguments by const reference instead of by value. To recap, passing a class argument by value causes a copy of the class to be made (which is slow) -- most of the time, we don’t need a copy, a reference to the original argument works just fine, and is more performant because it avoids the needless copy. We typically make the reference const in order to ensure the function does not inadvertently change the argument, and to allow the function to accept rvalue arguments (e.g. literals and temporary objects).
Can you figure out what’s wrong with the following code?
#include <iostream>
class Date
{
private:
int m_year{};
int m_month{};
int m_day{};
public:
Date(int year, int month, int day)
: m_year { year }
, m_month { month }
, m_day { day }
{
}
int getYear() { return m_year; }
int getMonth() { return m_month; }
int getDay() { return m_day; }
};
// note: We're passing date by const reference here to avoid making a copy of date
void printDate(const Date &date)
{
std::cout << date.getYear() << '/' << date.getMonth() << '/' << date.getDay() << '\n';
}
int main()
{
Date date{2016, 10, 16};
printDate(date);
return 0;
}
The answer is that inside of the printDate()
function, date
is treated as a const object (because it was passed by const reference). And with that const date
, we’re calling non-const member functions getYear()
, getMonth()
, and getDay()
. Since we can’t call non-const member functions on const objects, this will cause a compile error.
The fix is simple: make getYear()
, getMonth()
, and getDay()
const:
#include <iostream>
class Date
{
private:
int m_year{};
int m_month{};
int m_day{};
public:
Date(int year, int month, int day)
: m_year { year }
, m_month { month }
, m_day { day }
{
}
int getYear() const { return m_year; }
int getMonth() const { return m_month; }
int getDay() const { return m_day; }
};
// note: We're passing date by const reference here to avoid making a copy of date
void printDate(const Date &date)
{
std::cout << date.getYear() << '/' << date.getMonth() << '/' << date.getDay() << '\n';
}
int main()
{
Date date{2016, 10, 16};
printDate(date);
return 0;
}
Now in function printDate()
, const date
will be able to successfully call const member functions getYear()
, getMonth()
, and getDay()
.
For const objects, this
is a const pointer to a const value
The errors generated from attempting to call a non-const member on a const object can be a little cryptic:
error C2662: 'int Something::getValue(void)': cannot convert 'this' pointer from 'const Something' to 'Something &' error: passing 'const Something' as 'this' argument discards qualifiers [-fpermissive]
In lesson 15.1 -- The hidden “this” pointer and member function chaining, we discussed the hidden this
pointer. For non-const member functions, this
is a const pointer to a non-const value (meaning this
cannot be pointed at something else, but the object pointing to may be modified).
With const member functions, this
is a const pointer to a const value (meaning the pointer cannot be pointed at something else, nor may the object being pointed to be modified).
Therefore, when we call a non-const member function with a const object, the implicit this
function parameter is a const pointer to a non-const object, but the argument has type const pointer to a const object.
Converting a pointer to a const object into a pointer to a non-const object requires discarding the const qualifier, which cannot be done implicitly. The compiler error generated by some compilers reflects the compiler complaining about being asked to perform such a conversion.
Const members can not return non-const references to members
Because the this
pointer of a const member function is a const pointer to a const object, such a function can not return a const pointer to a non-const object, nor can it return a non-const reference. Doing either requires a conversion that would discard the const qualifier.
Const member functions can return const references to members (or less commonly, const pointers to const objects).
We’ll see an example of this in the next section.
Member function const and non-const overloading
Finally, although it is not done very often, it is possible to overload a member function to have a const and non-const version of the same function. This works because the const qualifier is considered part of the function’s signature, so two functions which differ only in their const-ness are considered distinct.
#include <string>
#include <string_view>
class MyString
{
private:
std::string m_str{};
public:
MyString(std::string_view str="")
: m_str{ str }
{
}
const std::string& str() const { return m_str; } // str() for const objects (returns const reference)
std::string& str() { return m_str; } // str() for non-const objects (returns non-const reference)
};
int main()
{
MyString s1{};
s1.str() = "Hi"; // calls non-const str();
const MyString s2{};
s2.str(); // calls const str();
return 0;
}
The const version of the str
function will be called on any const objects, and the non-const version of str
will be called on any non-const objects.
Overloading a function with a const and non-const version is typically done when the return value needs to differ in constness. This is pretty rare, and if you run into this situation, you should probably ask yourself whether returning a non-const reference (or pointer to non-const) to the caller is really such a good idea (as it probably violates the encapsulation of your class).