C++编译器允许循环定义吗?

22

在编写一些树形代码时,我犯了一个错误,在这个例子中,我将其简化为线性树。在main()函数中,我想将一个节点附加到我的树上,但是我不小心将其附加到了“root”而不是“tree.root”。然而,令人惊讶的是,它仍能够正常编译,而且我甚至可以调用节点上的方法。只有在访问"value"成员变量时才会出错。

我想知道的主要问题是为什么编译器没有捕捉到这个bug?

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

由于右边的“root”是一个明显未声明的变量。另外,出于好奇,如果编译器允许它们通过,循环定义是否有实际用途?以下是代码的其余部分:

#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;
}

输出:

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

7
int x = x + 1; 这段代码有什么问题?你原本期望它会发生什么? - KamilCuk
首先变量被初始化(在int std::shared_ptr的情况下使用空指针),然后执行赋值操作。尝试使用构造函数将不被允许。 - rkapl
2
@rkapl 不,变量在这种情况下直到 RHS 完成才被初始化。这个语句被称为 复制初始化 - Ruslan
@Ruslan -- 你是对的,谢谢。 - rkapl
2个回答

23

这是C++定义的一个不幸的副作用,即声明和定义是分开进行的。因为变量首先被“声明”,所以它们可以在自己的初始化中使用:

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

变量声明后,可以在完整定义自身的初始化中使用。

如果在AddLeaf中使用第二个参数的数据,由于变量未初始化,这将导致未定义行为


8
注意,这不是赋值操作,而是复制构造(或者更准确地说是复制初始化)。这个定义等同于 std::shared_ptr<Node> root(tree.AddLeaf(12, root)); 默认构造函数不会被调用。 - Some programmer dude
你是对的 - UB也可以包括使用默认构造函数...我太傻了。 - UKMonkey
4
编译器在这个问题上没有选择权。标准严格规定了构造函数的重载解析方式,仅根据参数数量就可以排除默认构造函数。在这方面效率不是一个因素。 - MSalters
7
只有在变量被读取写入时才会导致未定义行为。如果存储了它的地址或标识但从未读取或写入变量的值,则不会发生UB。然而,这种做法非常脆弱。 - Yakk - Adam Nevraumont
3
至少在C语言中(尽管我想不出任何使用C ++的好原因,除了与C的兼容性),有很多理由需要分别定义和声明:Foo *foo = malloc(sizeof(*foo)); 是一种常见的习惯用法,可以避免在重构代码时常见的陷阱。 - Voo
显示剩余3条评论

13

"root"在右边是一个明显未声明的变量。

它并不是未声明的。它正是通过同一语句声明的。但是,在调用AddLeaf(root)时,root尚未初始化,因此当函数内使用对象值(与null等进行比较)时,行为是未定义的。

是的,允许在声明中使用变量本身,但是不允许使用其值。你几乎只能使用地址或创建引用,或者处理子表达式类型的表达式,例如sizeofalignof

是的,虽然可能很少见,但确实有用例。例如,您可能想表示一个图,并且您可能具有一个构造函数,该构造函数以指向链接节点的指针作为参数,并且您可能希望能够表示连接自身的节点。因此,您可以编写Node n(&n)。我不会争论这是否是用于图形API的良好设计。


1
递归lambda是更合理的使用情况:http://coliru.stacked-crooked.com/a/1c4693c645d76d34 - R2RT

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接