Paradox
Paradox

Reputation: 2035

C++ Compiler allows circular definition?

I ran into the following oddity when making a mistake writing some code for trees. I've stripped down this example a lot so it is only a Linear Tree.

Basically, in the main() function, I wanted to attach a Node to my tree, but instead of attaching it to "tree.root", I attached it to just "root". However, to my surprise, not only did it all compile just fine, but I was able to call methods on the nodes. It only errored when I tried to access the "value" member variable.

I guess my main question is, why didn't the compiler catch this bug?

std::shared_ptr<Node> root = tree.AddLeaf(12, root);

Since "root" on the RHS is a flat-out undeclared variable. Also, out of curiosity, if the compiler lets them through, do circular definitions have an actual use case? Here's the rest of the code:

#include <iostream>
#include <memory>

struct Node
{
    int value;
    std::shared_ptr<Node> child;

    Node(int value)
    : value {value}, child {nullptr} {}

    int SubtreeDepth()
    {
        int current_depth = 1;
        if(child != nullptr) return current_depth + child->SubtreeDepth();
        return current_depth;
    }
};

struct Tree
{
    std::shared_ptr<Node> root;

    std::shared_ptr<Node> AddLeaf(int value, std::shared_ptr<Node>& ptr)
    {
        if(ptr == nullptr)
        {
            ptr = std::move(std::make_shared<Node>(value));
            return ptr;
        }
        else
        {
            std::shared_ptr<Node> newLeaf = std::make_shared<Node>(value);
            ptr->child = std::move(newLeaf);
            return ptr->child;
        }
    }
};


int main(int argc, char * argv[])
{

    Tree tree;
    std::shared_ptr<Node> root = tree.AddLeaf(12, root);
    std::shared_ptr<Node> child = tree.AddLeaf(16, root);

    std::cout << "root->SubtreeDepth() = " << root->SubtreeDepth() << std::endl; 
    std::cout << "child->SubtreeDepth() = " << child->SubtreeDepth() << std::endl; 

    return 0;
}

Output:

root->SubtreeDepth() = 2
child->SubtreeDepth() = 1

Upvotes: 22

Views: 2800

Answers (2)

eerorika
eerorika

Reputation: 238381

Since "root" on the RHS is a flat-out undeclared variable.

It's not undeclared. It is declared by that very same statement. However, root is uninitialised at the point where AddLeaf(root) is called, so when the value of the object is used (compared to null etc.) within the function, the behaviour is undefined.

Yes, using a variable in its own declaration is allowed, but using its value is not. Pretty much all you can do with it is take the address or create a reference, or expressions that only deal with type of the sub expression such as sizeof and alignof.

Yes, there are use cases although they may be rare. For example, you might want to represent a graph, and you might have a constructor for node that takes a pointer to linked node as an argument and you might want to be able to represent a node that links with itself. Thus you might write Node n(&n). I won't argue whether that would be a good design for a graph API.

Upvotes: 13

Some programmer dude
Some programmer dude

Reputation: 409216

That's an unfortunate side-effect of definitions in C++, that declaration and definition is done as separate steps. Because the variables are declared first, they can be used in their own initialization:

std::shared_ptr<Node> root = tree.AddLeaf(12, root);
^^^^^^^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^
Declaration of the variable  Initialization clause of variable

Once the variable is declared, it can be used in the initialization for the full definition of itself.

It will lead to undefined behavior in AddLeaf if the data of the second argument is used, as the variable is not initialized.

Upvotes: 23

Related Questions