Lei Mao bio photo

Lei Mao

Machine Learning, Artificial Intelligence, Computer Science.

Twitter Facebook LinkedIn GitHub   G. Scholar E-Mail RSS

Introduction

In C++ programming, when we implement the methods for classes, we usually add const specifier to make sure that the members of the class instance will not be modified by the method. However, it is also valid that we do const overloading for the same method, i.e., a class has methods that have the exact same method name and arguments, and one of them has a const specifier.


In this blog post, I would like to talk about the const overloading for C++ class methods.

Examples

In this example, I implemented a simple Vector class mimicking the std::vector class. We could see that operator[] overloading methods have two versions, T& operator[](int n) and const T& operator[](int n) const. The data methods also have two versions, T* data() and const T* data() const. The (second) const specifier ensures that calling the method will not modify the instance members.


Given a class which has const overloading for certain method func(), if the instance of the class is const, calling the method func() will invoke func() const, and if the instance of the class is not const, calling the method func() will invoke the func() without the const specifier.

#include <iostream>

template <typename T>
class Vector
{
public:

    // Default constructors
    Vector() : mSize{0}, mBuffer{nullptr}
    {
    }
    // Constructors which take more than zero arguments 
    explicit Vector(size_t size) : mSize{size}, mBuffer{new T[size]}
    {
        for (int i = 0; i < mSize; i ++)
        {
            mBuffer[i] = 0;
        }
    }
    explicit Vector(std::initializer_list<T> lst) : mSize{lst.size()}, mBuffer{new T[lst.size()]}
    {
        std::copy(lst.begin(), lst.end(), this->mBuffer);
    }
    // Copy constructor
    Vector(const Vector& vec) : mSize{vec.mSize}, mBuffer{new T[vec.mSize]}
    {
        std::copy(vec.mBuffer, vec.mSize, this->mBuffer);
    }
    // Move constructor
    Vector(const Vector&& vec) : mSize{vec.mSize}, mBuffer{vec.mBuffer}
    {
        vec.mSize = 0;
        vec.mBuffer = nullptr;
    }
    // Copy assignment
    Vector& operator=(const Vector& vec)
    {
        delete[] this->mBuffer;
        this->mBuffer = new T[vec.mSize];
        std::copy(vec.mBuffer, vec.mSize, this->mBuffer);
        this->mSize = vec.mSize;
        return *this; // This line of code is not required
    }
    // Move assignment
    Vector& operator=(const Vector&& vec)
    {
        delete[] this->mBuffer;
        this->mBuffer = vec.mBuffer;
        this->mSize = vec.mSize;
        vec.mSize = 0;
        return *this; // This line of code is not required
    }
    // Destructor
    ~Vector()
    {
        delete[] this->mBuffer;
    }
    // [] operator
    T& operator[](int n)
    {
        std::cout << "Non-const operator [] called." << std::endl;
        return this->mBuffer[n];
    }
    // [] operator const overloaded
    const T& operator[](int n) const
    {
        std::cout << "Const operator [] called." << std::endl;
        return this->mBuffer[n];
    }
    // Public methods
    size_t size() const
    {
        return this->mSize;
    }
    // For non-const typed instance, we return normal pointers
    T* data()
    {
        std::cout << "Non-const pointer returned." << std::endl;
        return this->mBuffer;
    }
    // const overloaded
    // For const typed instance, we return const pointers to prevent modifying the instance
    const T* data() const
    {
        std::cout << "Const pointer returned." << std::endl;
        return this->mBuffer;
    }

private:

    size_t mSize;
    T* mBuffer;
};

template <typename T>
void printConstVector(const Vector<T>& vec)
{
    for (int i = 0; i < vec.size(); i ++)
    {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;
}

template <typename T>
void printNonConstVector(Vector<T>& vec)
{
    for (int i = 0; i < vec.size(); i ++)
    {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;
}

int main()
{
    Vector<int> vec{1,2,3};
    printConstVector(vec);
    printNonConstVector(vec);

    std::cout << "------------------------" << std::endl;

    const Vector<int> constVec{1,2,3};
    printConstVector(constVec);

    std::cout << "------------------------" << std::endl;

    int* pVec = vec.data();

    std::cout << "------------------------" << std::endl;

    pVec[0] = 4;
    pVec[1] = 5;
    pVec[2] = 6;
    printNonConstVector(vec);

    std::cout << "------------------------" << std::endl;

    const int* pConstVec = constVec.data();
}

To compile the program, please run the following command in the terminal.

$ g++ const_overloading.cpp -o const_overloading --std=c++11

The expected output of the program would be as follows.

$ ./const_overloading 
Const operator [] called.
1 Const operator [] called.
2 Const operator [] called.
3 
Non-const operator [] called.
1 Non-const operator [] called.
2 Non-const operator [] called.
3 
------------------------
Const operator [] called.
1 Const operator [] called.
2 Const operator [] called.
3 
------------------------
Non-const pointer returned.
------------------------
Non-const operator [] called.
4 Non-const operator [] called.
5 Non-const operator [] called.
6 
------------------------
Const pointer returned.

What if the class does not have const overloading? There would be problems. For example, let’s remove the const overloadings (const T& operator[](int n) const and const T* data() const) from the Vector class. The compiler threw errors against us complaining that “‘const Vector’ as ‘this’ argument discards qualifiers".

$ g++ non_const_overloading.cpp -o non_const_overloading --std=c++11
non_const_overloading.cpp: In function ‘int main()’:
non_const_overloading.cpp:126:42: error: passing ‘const Vector<int>’ as ‘this’ argument discards qualifiers [-fpermissive]
     const int* pConstVec = constVec.data();
                                          ^
non_const_overloading.cpp:70:8: note:   in call to ‘T* Vector<T>::data() [with T = int]’
     T* data()
        ^~~~
non_const_overloading.cpp: In instantiation of ‘void printConstVector(const Vector<T>&) [with T = int]’:
non_const_overloading.cpp:105:25:   required from here
non_const_overloading.cpp:87:25: error: passing ‘const Vector<int>’ as ‘this’ argument discards qualifiers [-fpermissive]
         std::cout << vec[i] << " ";
                      ~~~^
non_const_overloading.cpp:59:8: note:   in call to ‘T& Vector<T>::operator[](int) [with T = int]’
     T& operator[](int n)
        ^~~~~~~~

This basically means that for a const class instance, it will look for methods with const specifiers when the methods are called. However, there is only one method without the const specifier declared and implemented, passing this const instance to the method without const specifier caused the problem.


If a class only has const methods, this is fine. A non-const class instance could call const methods.

Caveats

A const class method could return non-const values. For example, the following data() method returns const T* typed pointer. This means the returned pointer is const and we cannot modify the variable via dereferencing the pointer.

    const T* data() const
    {
        std::cout << "Const pointer returned." << std::endl;
        return this->mBuffer;
    }

However, if the data() method returns T* typed pointer, i.e., the implementation is

    T* data() const
    {
        std::cout << "Const pointer returned." << std::endl;
        return this->mBuffer;
    }

We could modify the variable via dereferencing the pointer! Although this is syntactically correct, sometimes the behavior is not what we really want. Given a const class instance, why would we ever want the instance to give us something that could modify its own content?

Conclusions

For some custom classes that we know we would create const instances or pass the instance by const values or references, const overloading is necessary.