Компилятор С++ допускает круговое определение?
Я столкнулся со следующей странностью, когда сделал ошибку при написании кода для деревьев. Я много раз сократил этот пример, так что это только линейное дерево.
По сути, в функции 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
Ответы
Ответ 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.