Компилятор С++ допускает круговое определение? - программирование

Компилятор С++ допускает круговое определение?

Я столкнулся со следующей странностью, когда сделал ошибку при написании кода для деревьев. Я много раз сократил этот пример, так что это только линейное дерево.

По сути, в функции main() я хотел присоединить Node к своему дереву, но вместо того, чтобы прикреплять его к "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
4b9b3361

Ответ 1

Это неприятный побочный эффект определений в C++, что объявление и определение выполняются как отдельные шаги. Поскольку переменные объявляются первыми, они могут использоваться для собственной инициализации:

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

Как только переменная объявлена, она может использоваться в инициализации для полного определения ее самой.

Это приведет к неопределенному поведению в AddLeaf если будут использованы данные второго аргумента, так как переменная не инициализирована.

Ответ 2

Так как "корень" в RHS является полной необъявленной переменной.

Это не объявлено. Об этом говорится в том же заявлении. Однако root неинициализируется в точке, где AddLeaf(root), поэтому, когда значение функции используется (по сравнению с нулевым значением и т.д.) Внутри функции, поведение не определено.

Да, использование переменной в ее собственном объявлении разрешено, но использование ее значения запрещено. Практически все, что вы можете с этим сделать, это взять адрес или создать ссылку, или выражения, которые имеют дело только с типом alignof таким как sizeof и alignof.

Да, есть варианты использования, хотя они могут быть редкими. Например, вы можете представлять граф, и у вас может быть конструктор для узла, который принимает указатель на связанный узел в качестве аргумента, и вы можете захотеть представлять узел, который связывается с самим собой. Таким образом, вы можете написать Node n(&n). Я не буду спорить, будет ли это хорошим дизайном для графического API.