c++qtvectorstd

Why does Qt's SimpleTreeModel example use std::vector<std::unique_ptr<>> instead of simple std::vector<>?


I'm using Qt 6.5 in a project and trying to refactor/improve a tree model that was initially written in Qt 5.10. I spent some time looking at Qt's SimpleTreeModel example and succesfully use the code to create my own model:

class TreeItem
{
public:
    explicit TreeItem(QVariantList data, TreeItem *parentItem = nullptr);

    void appendChild(std::unique_ptr<TreeItem> &&child);

    TreeItem *child(int row);
    int childCount() const;
    int columnCount() const;
    QVariant data(int column) const;
    int row() const;
    TreeItem *parentItem();

private:
    std::vector<std::unique_ptr<TreeItem>> m_childItems;
    QVariantList m_itemData;
    TreeItem *m_parentItem;
};

But something that bugs me is the use of std::vector<std::unique_ptr<TreeItem>> (std::vector<TreeItem*> in previous Qt versions). Why use that instead of using a more simple std::vector<TreeItem>?

I see no point using a pointer here, either basic or smart, as this adds a level of indirection that isn't necessary and could be costly for models with a lot of items.


Solution

  • Items in dynamic containers may get moved around. Each TreeItem needs to know its parent, in this data structure design. If m_childItems contained actual TreeItem objects, they would move to a different address when the vector gets resized. At that point, the parent pointers of their child would become dangling pointers. Some more complex/expensive method would be needed to keep track of parent items.

    So each TreeItem needs to stay at a fixed memory location once it is created. Simple solution is, if m_childItems contains pointers to items allocated from heap.

    Manual memory management is a big no-no in modern C++ code, and for a good reason, as that is a huge source of bugs in C and legacy C++ code. std::unique_ptr is the C++ class to store a pointer to an object which has a single owner and no shared ownership. When m_childItems gets destructed, all std::unique_ptrs in it are destructed too, automatically, and they delete the TreeItems.

    Another, in this case minor point is, TreeItem has non-trivial copy/move cost, so if m_childItems contained actual objects instead of pointers, then the vector getting resized would be slightly expensive. Copying/moving just pointers is a lot faster.

    So this is why m_childItems contains pointers, and why the pointers are std::unique_ptr in this case. A different use case or design might use/need std::shared_ptr and std::weak_ptr, but if there is no need for that, owning std::unique_ptr and raw non-owning pointers is simpler and better.