shared_ptr、unique_ptr、所有权,在这种特殊情况下,我是否做得过头了?

shared_ptr, unique_ptr, ownership, Am I overdoing it in this particular case?

我在图形应用程序上工作,并且一直在使用共享指针和唯一指针,主要是因为它为我处理内存重新分配(又名方便),这可能很糟糕(如果这就是我使用它们的原因)。

我最近在 Whosebug 上阅读了一个问题的答案,其中提到根据 B. Stroustrup 的说法,通常不应使用 unique/shared 指针,而应改为按值传递参数。

我有一个图形案例,我认为使用 shared_ptr 是有意义的,但我想从专家那里知道(我不是 C++ 专家)我是否超过了 doing/thinking 它并且如果是这样,他们会做什么(以符合 C++ 推荐和效率)

我就拿一个出现在Rendering/Ray-Tracing中的一般问题来说吧。在这个特定的问题中,我们有一个对象池(我们将使用三角形来进行解释)和一个结构,为了解释的简单性,我们将其称为常规 3D 网格。假设在某个时候我们需要将三角形插入网格:这意味着我们需要检查每个插入的三角形的边界体积是否与网格中的任何单元格重叠,它确实如此,然后每个重叠的单元格需要为该三角形保留一个 pointer/reference (供以后使用)。一个三角形可能与 1 个以上的单元格重叠,因此它可以被多个单元格引用多次(你看我在这里用 shared_ptr 去哪里)。

请注意,在网格结构之外,我们不需要三角形池(因此从技术上讲,这里拥有三角形池的对象是网格,或者更准确地说是网格的单元格)。

class Grid
{
    struct Cell
    {
        std::vector<std::shared_ptr<const Triangle>> triList; 
    };
    void insert(triangle*& tri_)
    {
        std::shared_ptr<const Triangle> tri = tri_;
        for (each cell overlapped by tri) {
            // compute cell index
            uint32_t i = ...
            cells[i].triList.push_back(tri);
        }
    }
    Cell cells[RES * RES * RES];
};

void createPoolOfTrianglesAndInsertIntoGrid()
{
    Grid grid;
    uint32_t maxTris = 32;
    Triangle* tris = new Triangles[maxTris];
    // process the triangles
    ...
    // now insert into grid
    for (uint32_t i = 0; i < maxTris; ++i) {
        // use placement new
        Triangle* tri = new (&tris[i]) Triangle;
        grid.insert(tri);
    }
    // no need to delete tris here ... it should be done by 
    // the grid when we go out of this function's scope
}

这听起来很复杂,但我在这个设计背后的动机是遵循 Stroustrup 的建议。在这种情况下,函数 createPoolOfTrianglesAndInsertIntoGrid 不拥有三角形,它是网格的单元格。所以我在函数 createPoolOfTrianglesAndInsertIntoGrid 中分配内存,因为这是我需要创建三角形的地方,然后我使用 placement new 方法获取指向该池中每个三角形的指针,然后我可以将其传递给网格insert 方法(我将该对象的内存管理推迟到该方法)。在那里,它将三角形转换为 shared_ptr 并且单元格现在可以与其共享 "reference"(使用 shared_ptr)。

我想知道在您看来,这是否朝着正确的方向发展,或者这是否看起来完全错误,无论是在实施方面还是在可能的效率损失方面(我分配了一个内存池,然后使用新位置创建一个临时三角形,然后将其传递给网格插入方法,然后转换为 shared_ptr, ...)

我正在努力学习和改进我的代码,希望您能提供宝贵的专业反馈。

编辑:基本上我正在尝试找到解决该问题的正确方法 + 我将尝试稍后根据您的评论提供一些修改

高效的内存分配是对象生命周期和对象行为的一个单独问题。

控制分配策略的机制是 Allocator in std::vector<Type, Allocator>std::allocate_shared<Type, Allocator>

您似乎想从池中执行分配。

首先,赋予 Triangle 值语义(即不持有指向它的指针)将允许 std::vector 高效地分配内存块。它的分配策略假定需要多个对象。在幕后,它分配内存块并根据需要就地调用 constructors/destructors。

如果 Triangle 确实需要共享句柄(即 shared_ptr),那么您可以使用自定义分配器分配 shared_ptr。

Boost 有一些内存池分配器示例。

在我看来你的 Grid 拥有三角形。我假设三角形相对较轻(3-5 维?)。

我认为这样的东西可能适合。我在 Grid 中使用 容器 来按值 取得三角形的所有权 容器将在Grid超出范围时删除三角形。

然后每个 Cell 只使用原始指针来跟踪它引用的三角形。 Cell 不拥有三角形,它们只是保存指向 Grid.

所拥有的三角形的指针
class Grid
{
    struct Cell
    {
        std::vector<Triangle*> triList; // non owning
    };

    void insert(Triangle tri) // pass by value
    {
        tris.push_back(tri); // Grid owns this by value

        for(each cell overlapped by tri) {
            // compute cell index
            uint32_t i = ...

            cells[i].triList.push_back(&tris.back());
        }
    }

    // Use a deque because it won't re-allocate when adding
    // new elements to the end
    std::deque<Triangle> tris; // elements owned by value

   Cell cells[RES * RES * RES]; // point to owned elements
};

void createPoolOfTrianglesAndInsertIntoGrid()
{
    Grid grid; // owns the triangles (by value)

    uint32_t maxTris = 32;

    std::vector<Triangle> tris(maxTris);
    // process the triangles
    // ...
    // now insert into grid
    for(auto tri: tris)
        grid.insert(tri);
    }

    // no need to delete tris here ... it should be done by
    // the grid when we go out of this function's scope
}

注意: 我使用 std::dequeGrid 中按值存储三角形。那是因为它在添加新三角形时永远不会重新分配其内部存储器。如果您在此处使用 std::vector,那么当 std::vector 自行调整大小时,您的原始指针最终会悬空。

或者:

鉴于看起来您在函数中构建了所有三角形,然后将它们全部传递给 Grid,为什么一次只做一个?你可以一次性把整个容器递进去。如果您使用 移动语义 执行此操作,您甚至不必复制任何内容:

class Grid
{
    struct Cell
    {
        std::vector<Triangle*> triList; // non owning
    };

    // Accept the entire container in-tack
    // (no reallocations allowed after this point)
    void insert(std::vector<Triangle> tris) // pass by value (able to move in)
    {
        //
        for(auto& tri: tris)
        {
            for(each cell overlapped by tri) {
                // compute cell index
                uint32_t i = ...

                cells[i].triList.push_back(&tri);
            }
        }
    }

    // Using a vector so it MUST NOT be resized after
    // Cells have been pointed to its elements!!!
    std::vector<Triangle> tris; // elements owned by value
    Cell cells[RES * RES * RES]; // point to owned elements
};

void createPoolOfTrianglesAndInsertIntoGrid()
{
    Grid grid; // owns the triangles (by value)

    uint32_t maxTris = 32;

    // Build the triangles into
    std::vector<Triangle> tris(maxTris);
    // process the triangles
    // ...
    // now insert into grid
    grid.insert(std::move(tris)); // move the whole darn container (very efficient)

    // no need to delete tris here ... it should be done by
    // the grid when we go out of this function's scope
}

注意: 现在我使用了 std::vector,因为你不会在三角形到达 Grid 后一个一个地添加它们。但是你必须确保 inside Grid 拥有的 `std::vector 没有调整大小。

如果单元格是三角形的唯一所有者,则不需要池,只需将三角形分配为 shared_ptr 并将它们插入网格即可。这样,所有不再属于任何单元格的三角形都将被清除。