0

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.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
  • 4
    The basic rule-of-thumb is that if you have to use `const_cast` to make your program compile, you're almost always doing something wrong. It works in the reverse of what you think. It _removes_ const qualifiers, not adds them. – paddy Mar 28 '23 at 14:48
  • 2
    "the node won't change parent" - exactly. And that's why the `pParent` argument to the ctor should be made `const`. Which will in turn make the `const_cast` superfluous. – Friedrich Mar 28 '23 at 15:09
  • 3
    "because it is using "this" then I have to put the member `m_pParent` const" - no, this idea is wrong, and everything that follows seems based on this wrong premise. Note that in `CNode* CreateChild(int n) const`, `m_Parent` is `const`, but not `*m_Parent` ! It's a `CNode *const``, not a `constCNode *` – MSalters Mar 28 '23 at 15:09
  • @Friedrich Thank you Ok it should be const but I don't see how to get rid of the const_cast in "CreateChild". – Calivernon Mar 29 '23 at 12:46
  • @MSalters Thank you yes it is not clear so I just read an article "difference between “const X* p”, “X* const p” and I need to read it again and again ... but so far I don't see how to manage the "this" issue if not with a const_cast. – Calivernon Mar 29 '23 at 12:46

1 Answers1

0

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).

Friedrich
  • 2,011
  • 2
  • 17
  • 19
  • Thank you, yes I see it is working but I still have the issue (const_cast needed) in the main when looping on the nodes, I tried with godbolt (very nice tool ;) ): [https://godbolt.org/z/GsY19hza3](https://godbolt.org/z/GsY19hza3) – Calivernon Mar 29 '23 at 19:14
  • @Calivernon edited my question to answer the new and different problem you brought up in your comment. In the future, please post a new question. Better yet, get a good book on C++ and read about constness. – Friedrich Mar 30 '23 at 05:32
  • I appreciate your help and you answered partially my initial question, but my first question is not answered yet: "How to put the member const properly without using a const_cast" that is the questioning I have and gave details in first post. Thanks. – Calivernon Mar 30 '23 at 08:26
  • 1
    Thank you it is very clear, it explains everything and solves all my issues. – Calivernon Mar 31 '23 at 13:05