提问者:小点点

为什么std::vector调用析构函数而留下不同的作用域?


我有这样的代码

std::vector<cuarl_path::Path> RoverPlanner::get_paths(const char *filename) const
{
    pugi::xml_document doc;
    doc.load_file(filename);
    pugi::xml_node root = doc.document_element();

    std::vector<cuarl_path::Path> paths;
    for (auto path_node : root.children()) {
        std::vector<cuarl_path::Segment*> segments;
        for (auto segment_node : path_node.children())
        {
            //do stuff to populate the `segments` vector
        }

        cuarl_path::Path* path = new cuarl_path::Path(segments);
        paths.push_back(*path); //Path destructor called here
    }

    return paths;
}

问题似乎是std::vector“paths”调用了它的析构函数,但我不明白为什么。 范围还没有结束。


共1个答案

匿名用户

paths.push_back(*path);

这可以做一些事情。

>

  • 它从堆分配的*path对象构造path副本。

    它调整路径向量的大小。 这有时只需要增加一个整数,但有时则需要移动所有现有的路径对象并销毁旧的对象。

    在第一点,你有一个漏洞。 new在空闲存储区上分配对象,您始终负责确保它们被清理。 将对象从空闲存储复制到向量中不会清理空闲存储中的对象。

    在第二点中,vector实际上保存的是实际对象,而不是对它们的引用。 因此,当您调整缓冲区的大小时(push_back可以这样做),您必须移动对象的值,然后清除要丢弃的对象。

    那个清理就是你正在做的析构函数。

    你似乎是一个C#或Java程序员。 在这两种语言中,对象的实际值确实很难创建--它们希望保留对对象的垃圾收集引用。 对象数组实际上是对那些语言中的对象的引用数组。 在C++中,对象向量是一个实际包含所讨论对象的向量。

    您使用new也是一个提示。 那里不需要new,也不需要指针:

    std::vector<cuarl_path::Path> RoverPlanner::get_paths(const char *filename) const
    {
        pugi::xml_document doc;
        doc.load_file(filename);
        pugi::xml_node root = doc.document_element();
    
        std::vector<cuarl_path::Path> paths;
        for (auto&& path_node : root.children()) {
            std::vector<cuarl_path::Segment*> segments;
            for (auto segment_node : path_node.children())
            {
                //do stuff to populate the `segments` vector
            }
    
            cuarl_path::Path path = cuarl_path::Path(segments);
            paths.push_back(std::move(path)); //Path destructor called here
        }
    
        return paths;
    }
    

    您仍然会在行中调用路径析构函数(实际上,您会得到一个额外的),但代码不会泄漏。 假设您的move构造函数是正确的(并且实际上正确地移动了路径的状态),那么一切都应该可以工作。

    现在一个更高效的版本看起来像:

    std::vector<cuarl_path::Path> RoverPlanner::get_paths(const char *filename) const
    {
        pugi::xml_document doc;
        doc.load_file(filename);
        pugi::xml_node root = doc.document_element();
    
        std::vector<cuarl_path::Path> paths;
        auto&& children = root.children();
        paths.reserve(children.size());
        for (auto path_node : children) {
            std::vector<cuarl_path::Segment*> segments;
            for (auto segment_node : path_node.children())
            {
                //do stuff to populate the `segments` vector
            }
    
            paths.emplace_back(std::move(segments));
        }
    
        return paths;
    }
    

    它可以消除所有您正在处理的临时变量,并在资源不再有用时移动它们。

    假设move构造函数是有效的,这里的最大收获是我们预先研究了路径向量(节省了lg(n)内存分配),并且我们将段向量移动到路径的构造函数中(如果编写正确,这可以避免不必要地复制段指针的缓冲区)。

    这个版本也没有在有问题的行上调用析构函数,但是我认为这并不是特别重要的; 一个空路径的析构函数的代价应该几乎是免费的,甚至可以优化掉。

    我还可能避免了path_node对象的副本,这可能是值得避免的,具体取决于它的编写方式。