【问题标题】:C++ Compiler allows circular definition?C++ 编译器允许循环定义?
【发布时间】:2019-01-07 10:57:43
【问题描述】:

在为树编写一些代码时出错,我遇到了以下奇怪情况。我已经对这个例子做了很多剥离,所以它只是一个线性树。

基本上,在 main() 函数中,我想将一个节点附加到我的树上,但我没有将它附加到“tree.root”,而是将它附加到“root”。然而,令我惊讶的是,它不仅编译得很好,而且我能够在节点上调用方法。只有当我尝试访问“value”成员变量时才会出错。

我想我的主要问题是,为什么编译器没有捕捉到这个错误?

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

由于 RHS 上的“根”是一个完全未声明的变量。另外,出于好奇,如果编译器允许它们通过,循环定义是否有实际用例?以下是其余代码:

#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

【问题讨论】:

  • int x = x + 1; 什么错误?你预计会发生什么?
  • 首先初始化变量(int std::shared_ptr 的情况为空指针),然后执行赋值。尝试使用构造函数,它不会被允许。
  • @rkapl 不,在这种情况下,直到 RHS 完成,变量才会被初始化。该声明称为copy initialization
  • @Ruslan -- 你是对的,谢谢。

标签: c++ c++14


【解决方案1】:

这是 C++ 中定义的一个不幸的副作用,即声明和定义是作为单独的步骤完成的。因为变量是先声明的,所以可以在自己的初始化中使用:

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

一旦声明了变量,就可以在初始化时使用它来完整定义自身。

如果使用第二个参数的数据会导致AddLeaf中的undefined behavior,因为变量没有初始化。

【讨论】:

  • 在这种情况下 - 将首先调用 root 的默认构造函数;然后添加叶子;然后等于运算符。我认为这里没有任何 UB。
  • @UKMonkey 请注意,这不是赋值,而是复制构造(或者更确切地说是copy initialization)。定义等于std::shared_ptr&lt;Node&gt; root(tree.AddLeaf(12, root)); 不会调用默认构造函数。
  • @UKMonkey:编译器在这件事上别无选择。该标准严格定义了构造函数的重载解析,仅参数的数量就排除了默认构造函数。效率不是其中的一个因素。
  • 只有当变量被 readwritten 到时才会导致未定义的行为。如果存储了它的地址或标识,但从未读取或写入变量的值,则不会发生 UB。然而,它非常脆弱。
  • 至少在 C 中(除了与 c 的原始兼容性之外,在 c++ 中想不出任何好的理由)虽然有充分的理由单独定义和声明:Foo *foo = malloc(sizeof(*foo)); 是一个常见的习惯用法,并且在重构代码时避免常见的陷阱。
【解决方案2】:

由于 RHS 上的“根”是一个完全未声明的变量。

它不是未声明的。它是由同样的声明声明的。但是,root 在调用 AddLeaf(root) 时未初始化,因此当在函数中使用对象的值(与 null 等相比)时,行为未定义。

是的,允许在自己的声明中使用变量,但不允许使用其值。几乎所有你能做的就是获取地址或创建一个引用,或者只处理子表达式类型的表达式,例如 sizeofalignof

是的,有一些用例,尽管它们可能很少见。例如,您可能想要表示一个图,并且您可能有一个 node 的构造函数,它将指向链接节点的指针作为参数,并且您可能希望能够表示一个与其自身链接的节点。因此你可以写Node n(&amp;n)。对于图形 API,我不会争论这是否是一个好的设计。

【讨论】:

猜你喜欢
  • 2013-09-06
  • 2011-04-01
  • 2013-07-14
  • 1970-01-01
  • 2012-12-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多