Advice about const (member and function)

88 views Asked by At

I am studying tree exploration, I implemented a class "CNode" for the nodes where each node has a pointer member to point to his parent, my question is about a const function and a const member, see below in my code.

So below is a simplified version of the main and of the class.

class CNode
{
private:
    int                 m_nID;
    CNode*              m_pParent;

public:

    CNode() : m_nID(0), m_pParent(nullptr) {}
    CNode(int n, CNode* pParent) : m_nID(n), m_pParent(pParent) {}

    CNode*  GetParent() const { return m_pParent; }
    void    PrintID() const { cout << m_nID << endl; }

    CNode* CreateChild(int n)
    {
        CNode* pNode = new CNode(n, this);
        return pNode;
    }
};


int main()
{
    CNode* pNodeRoot  = new CNode(0, nullptr);
    CNode* pNode = new CNode(1, pNodeRoot);

    while (pNode != nullptr)
    {
        pNode->PrintID();
        pNode = pNode->GetParent();
    }

    delete pNodeRoot;
    delete pNode;

    return 0;
}

So that is working but I am not satisfied because the member function:

CNode* CreateChild(int n)

should be const, so let's put it const but because it is using "this" then I have to put the member "m_pParent" const (which seems normal as in my case the node won't change parent), at the end it looks like that:

class CNode
{
private:
    int                 m_nID;
    const CNode*        m_pParent;

public:

    CNode() : m_nID(0), m_pParent(nullptr) {}
    CNode(int n, CNode* pParent) : m_nID(n), m_pParent(pParent) {}

    CNode* CreateChild(int n) const
    {
        CNode* pNode = new CNode(n, const_cast<CNode*>(this));
        return pNode;
    }

    const CNode*  GetParent() const { return m_pParent; }
    void          PrintID() const { cout << m_nID << endl; }
};

and then I have to change one line in the main using a const_cast:

pNode = const_cast<CNode*>(pNode->GetParent());

and it is working as well.

Both versions are working, I would like your advice to know:

  • what is the best practice,
  • if using const_cast is ok or not (I thought that was to avoid as much as possible),
  • if there is another way (simpler or neater) to do it.

Thank you.

1

There are 1 answers

4
Friedrich On

You cannot have a member be both const and mutable at the same time. You have to pick either and live with the consequences. Constness is a bit of a viral thing. If you expose m_pParent as a non-const pointer anywhere, it must not be const. Changing a const value is undefined behavior.

You may, however, cast to const to your heart's desire. So you can have a mutable CNode* m_pParent and decide to only hand out a const pointer.

It is possible to overload on constness, so you could have two getters like this:

CNode*       m_pParent; // no const before the *!

const CNode* GetParent() const;
CNode*       GetParent();

The non-const overload could only be called on a non-const CNode.

One of the reasons const was introduced in the first place was to keep programmers from accidentally changing their own variables. See it as a tool in your tool box. So when the compiler complains about const violations, you should listen.

In your case, it was because the child's GetParent() would expose a mutable pointer and thus violate constness.

Let's assume there's non-const method CNode::SetID(int) and look at the following code:

const CNode foo; // can only call const methods on foo
foo.SetID(123); // ERROR! const violation!
foo.CreateChild(1)->GetParent()->SetID(123); // ERROR! you would change foo through its child.

Because it was brought up in a comment: the meaning of const in relation to * is: const data * const pointer. It's in alphabetical order (data, pointer) if that serves as a mnemonic.

Below is a const-correct version of your code snippet with a const CNode* m_pParent:

class CNode
{
private:
    int                 m_nID;
    const CNode* const  m_pParent; // This is just a freebie.

public:

    CNode() : m_nID(0), m_pParent(nullptr) {}
    // Ctor now accepts a const CNode*. Everything is const now, no need for a const_cast
    CNode(int n, const CNode* pParent) : m_nID(n), m_pParent(pParent) {}

    CNode* CreateChild(int n) const
    {
        CNode* pNode = new CNode(n, this); // We use "this" without a const_cast :)
        return pNode;
    }

    const CNode*  GetParent() const { return m_pParent; }
    void          PrintID() const { std::cout << m_nID << "\n"; }
};

Or try it yourself on https://godbolt.org/z/4xs15P38a

If, on the other hand, you need to have a CNode* GetParent() and thus expose a mutable pointer to the parent, then CreateChild() cannot be const, plain and simple.

You asked three more questions:

what is the best practice

Use const wherever possible but not more often than that.

if using const_cast is ok or not (I thought that was to avoid as much as possible)

Yes. As a rule of thumb: avoid it! It is a part of the language specification and there are legitimate uses for it. It is preferable to C-style casts.

if there is another way (simpler or neater) to do it.

You have to decide if m_pParent is to be const or not and go with one of the decisions and everything it brings with it.

Solution to the new problem brought up in the comment: https://godbolt.org/z/1crbxMxco

If a method returns a const T* you need a const T* to assign the return value to. There's only an implicit cast to const (for obvious reasons).