4

在 C++ 中,假设我有一个类可以创建类似二叉树的结构,我使用它是这样的:

CTreeRoot* root = new CTreeRoot(/* whatever */);
CNode* leftNode = root->getLeftNode();
CNode* rightNode = root->getRightNOde();

leftNode->doSomething();
rightNode->doSomething();

// etc

并假设左右节点有自己的左右节点(因此,二叉树)。现在我想把它暴露给 Lua(使用 luabind),所以我可以做同样的事情:

local root = treeroot.new(/* whatever */)
local left = root:getLeftNode()
local right = root:getRightNode()

left:doSomething();
right:doSomething(); 

我已经完成了大部分工作。但是,对于 getLeftNode() 和 getRightNode() 方法,我很确定我做错了。以下是我在 C++ 中实现 getLeftNode() 的方式,例如:

int MyLua::TreeRootGetLeftNode(luaState* L)
{
    CTreeRoot* root = (CTreeRoot*)luaL_checkudata(L, 1, "MyLua.treeroot");
    CNode* leftNode = root->getLeftNode();

    if (leftNode != NULL)
    {
        int size = sizeof(CNode);

        // create a new copy of the CNode object inplace of the memory Lua
        // allocated for us
        new ((CNode*)lua_newuserdata(L,size)) CNode((const CNode&)*leftNode);
        lua_setmetatable(L, "MyLua.treenode");
    }
    else
    {
        lua_pushnil(L);
    }

    return 1;
}

我将 userdata 转换回 CTreeRoot 对象,调用 getLeftNode(),确保它存在,然后(这里是“错误的部分”)创建另一个 userdata 数据对象,并使用复制构造函数复制我想要返回的对象。

这是这种情况的“标准做法”吗?似乎您希望避免创建对象的另一个副本,因为您真正想要的只是对已经存在的对象的引用。

听起来这将是 lightuserdata 的理想场所,因为我不想创建新对象,我很乐意返回已经存在的对象。但是,问题是 lightuserdata 没有元表,所以一旦我把它们拿回来,这些对象对我来说就毫无用处了。基本上,我想做类似的事情:

int MyLua::TreeRootGetLeftNode(luaState* L)
{
    CTreeRoot* root = (CTreeRoot*)luaL_checkudata(L, 1, "MyLua.treeroot");
    CNode* leftNode = root->getLeftNode();

    if (leftNode != NULL)
    {
        // "WRONG" CODE BUT SHOWS WHAT I WISH I COULD DO
        lua_pushlightuserdata(L, (void*)leftNode);
        lua_setmetatable(L, "MyLua.treenode");
    }
    else
    {
        lua_pushnil(L);
    }

    return 1;
}

有人可以告诉我如何让我的MyLua::TreeRootGetLeftNode方法将已经存在的对象的副本返回给 Lua,以便我可以将该对象用作 Lua 中的“对象”吗?

4

1 回答 1

2

这里可以执行两个级别的内存优化。在您的第一个功能但效率低下的解决方案中,当用户调用 时getLeftNode(),它必须创建一个副本CNode以便将其存储在 Lua 用户数据中。此外,每次用户getLeftNode()在同一棵树上重复调用时,它都会不断创建新的用户数据来表示,CNode即使它之前已经创建过。

在第一级优化中,你可以记住这个调用,这样每次用户请求相同的子树时,你可以简单地返回最初创建的用户数据,而不是复制和构造另一个用户数据来表示相同的东西。不过,有 3 种方法可以解决此问题,具体取决于您是要修改 Lua 接口、更改 C++ 实现还是硬着头皮。

  • MyLua.treenodeuserdata 当前包含对象的实际数据CNode。然而,这是不幸的,因为这意味着每当你创建一个CNode对象时,你必须在创建时立即使用placement new 将它存储到Lua 分配的内存中。可能更好的是在用户数据中简单地存储一个指针(CNode*MyLua.treenode。这确实需要您修改 Lua 接口,MyLua.treenode以便它现在将其数据视为指向CNode对象的指针。

  • 如果您希望将CNode数据MyLua.treenode直接存储在 userdata 中,那么您必须确保在创建 时CTreeRoot,它会使用placement newCNode从 Lua 每次分配的内存中构造 s(或者您可以使用分配器模式在 C++ 标准库中使用?)。然而,这不太优雅,因为您的CNode实现现在取决于 Lua 运行时,我不推荐这样做。

  • 如果上述解决方案都不合适,那么您只需在返回子节点时进行复制,尽管您仍然可以通过跟踪是否创建相同的用户数据来提高在同一节点上重复调用的效率之前(例如,使用 Lua 表)。

在第二级优化中,您可以通过使复制的子树成为对原始树片段的弱引用来进一步节省内存。这样,每当您复制子树时,您只是在创建指向原始树的一部分的指针。或者,如果您希望子树即使在原始树被破坏后仍然存在,您可以使用强引用,但是您将不得不进入引用计数的血腥细节。无论哪种情况,这种优化纯粹是在 C++ 级别上,与 Lua 接口无关,从您的代码来看,我假设您已经在使用弱引用 ( CNode*)。

注意:除了在内部实现中使用外,最好避免使用轻量级用户数据。原因是轻量级用户数据本质上等同于 C 指针,因此几乎可以指向任何东西。如果您将轻量级用户数据暴露给 Lua 代码,您将不知道轻量级用户数据可能来自哪里或它包含什么类型的数据,使用它会带来安全风险(以及导致程序段错误的可能性)。使用轻量级用户数据的适当方法是将其用作存储在 Lua 注册表中的 Lua 查找表的索引,可用于实现前面提到的记忆。

于 2013-02-09T19:06:04.087 回答