PAYATHON 2026

为什么使用 auto 接收 std::vector 中的节点后,子节点没有出现在树中?

数据小陈

原因:auto 接收的是节点副本

子节点没有出现在输出中,是因为 auto alpha = root.add("alpha"); 得到的是一个独立的 Node 副本。后来添加的 "one"、"two" 和 "three" 都在这个局部副本里,root.dump() 遍历不到它们。

局部变量 alpha 离开作用域时,这些子节点才会析构,并没有在 root.dump() 之前被过早销毁。beta 和 gamma 的情况相同。

虽然 add() 返回 Node&,但普通 auto 声明不会保留返回表达式的引用属性:

auto alpha = root.add("alpha");  // alpha 的类型是 Node,发生拷贝
auto& alpha = root.add("alpha"); // alpha 的类型是 Node&,引用树中的节点

原代码中的 Node 可以被隐式拷贝,拷贝时会复制它的 _name 和 _subnodes。刚添加的节点还没有子节点,所以局部副本最初也没有子节点;之后对副本的修改不会同步到树中的节点。

修改方式:使用 auto& 操作树中的节点

保留原来的 Node 定义,将 main() 改为下面的写法。每个引用只在对应的代码块内使用,后续向 root 添加兄弟节点时,不会再使用之前的引用。

int main()
{
    auto root = Node("root");

    {
        auto& alpha = root.add("alpha");
        alpha.add("one");
        alpha.add("two");
        alpha.add("three");
    }

    {
        auto& beta = root.add("beta");
        beta.add("four");
        beta.add("five");
        beta.add("six");
    }

    {
        auto& gamma = root.add("gamma");
        gamma.add("seven");
        gamma.add("eight");
        gamma.add("nine");
    }

    root.dump();
    return 0;
}

这段代码的输出应为:

root
 alpha
  one
  two
  three
 beta
  four
  five
  six
 gamma
  seven
  eight
  nine

代码中使用的 std::string_view,以及返回元素引用的 std::vector::emplace_back(),要求使用 C++17 或更新的语言标准。

注意 std::vector 扩容后的引用失效

使用 auto& 后,还要留意节点的存储位置可能发生变化。例如:

auto& alpha = root.add("alpha");
root.add("beta");

// 如果上一次插入导致 root._subnodes 重新分配存储空间,
// alpha 已失效,下面的访问会产生未定义行为。
alpha.add("one");

std::vector 重新分配存储空间时,指向其中元素的引用、指针和迭代器都会失效。插入时是否发生重新分配,取决于容量是否足够。

前面的修复示例在添加下一个兄弟节点之前,已经完成了对当前节点的操作。alpha.add() 修改的是 alpha._subnodes,不会因此让存储在 root._subnodes 中的 alpha 本身失效。

如果需要跨多次插入长期保存节点引用,可以考虑使用 std::vector<std::unique_ptr<Node>>。容器重新分配时移动的是智能指针,所指向的节点仍位于原地址;但节点被删除或所属树被销毁后,引用仍会失效。已知节点数量时,也可以提前调用 reserve();只有后续插入不超过预留容量,它才能避免重新分配。

为什么析构日志可能让人误判?

向 root._subnodes 添加节点时,如果发生重新分配,容器会在新存储区构造元素,并销毁旧存储区中的元素。因此,同一个 _name 出现析构日志,并不意味着树中已经没有对应节点。

原代码还声明了析构函数 ~Node(),这会阻止编译器隐式生成移动构造函数;隐式拷贝构造函数仍然可用。对于这个定义,扩容时可以通过拷贝构造新位置的节点,再销毁旧位置的节点。

子节点未出现在输出中的直接原因是普通 auto 创建了局部副本。检查对象身份时,可以同时输出 this 地址和 _name,区分名称相同的不同对象。

备注:内容仅供参考。