sol2 icon indicating copy to clipboard operation
sol2 copied to clipboard

Using std::shared_ptr as table keys?

Open Leadwerks opened this issue 1 month ago • 1 comments

I am having trouble getting tables to recognize that different STL shared pointers to the same object are the same object:

-- This will not remove the key
self.manager.zombies[self] = nil

-- This will remove the key
for k, v in pairs(self.manager.zombies) do
	if k == self then
		self.manager.zombies[k] = nil
	end
end

I tried declaring all of these meta functions in the class definition, but to no avail:

			sol::meta_function::equal_to, sol::overload(
				[](Model& self, Model* other) { return &self == other; },
				[](Model& self, Entity* other) { return &self == other; },
				[](Model& self, Object* other) { return &self == other; }
			),
			sol::meta_function::equal_to, sol::overload(
				[](Model& self, shared_ptr<Model> other) { return &self == other.get(); },
				[](Model& self, shared_ptr<Entity> other) { return &self == other.get(); },
				[](Model& self, shared_ptr<Object> other) { return &self == other.get(); }
			),
			sol::meta_function::less_than, sol::overload(
				[](Model& self, Model* other) { return &self < other; },
				[](Model& self, Entity* other) { return &self < other; },
				[](Model& self, Object* other) { return &self < other; }
			),
			sol::meta_function::less_than, sol::overload(
				[](Model& self, shared_ptr<Model> other) { return &self < other.get(); },
				[](Model& self, shared_ptr<Entity> other) { return &self < other.get(); },
				[](Model& self, shared_ptr<Object> other) { return &self < other.get(); }
			),
			sol::meta_function::less_than_or_equal_to, sol::overload(
				[](Model& self, Model* other) { return &self <= other; },
				[](Model& self, Entity* other) { return &self <= other; },
				[](Model& self, Object* other) { return &self <= other; }
			),
			sol::meta_function::less_than_or_equal_to, sol::overload(
				[](Model& self, shared_ptr<Model> other) { return &self <= other.get(); },
				[](Model& self, shared_ptr<Entity> other) { return &self <= other.get(); },
				[](Model& self, shared_ptr<Object> other) { return &self <= other.get(); }
			),

Any advice on how to get the table compare to recognize that two different shared pointers to the same object are the same?

Leadwerks avatar Nov 17 '25 17:11 Leadwerks

As far as I understand, every time you wrap some C++ pointer or reference in a userdata, it will be a different userdata which is inequal to previously-created references. Userdata equality metamethods are not used to check uniqueness of table keys and cannot work around this problem. It's possible to avoid this issue by using lightuserdata, but I wouldn't recommend that approach as you'd need to create an elaborate type-checking system to get anywhere with it.

What I did in my game engine was to ensure that each entity or component is only wrapped in a sol::userdata once during its lifetime. Then, using sol's customization functions I made it so that pushing those types to Lua calls a method that makes a copy of that userdata reference. This approach would be difficult to generalize to arbitrary shared pointers, though... You'll need to keep your wrappers somewhere and the easiest place is within the entities being referenced.

Here's a simplification of my "provide self reference" code. Here I'm using stateless_reference instead of reference for the sole purpose of making my objects take up a bit less memory, which creates some safety issues, so you can ignore that part.

const sol::reference& Object::as_lua(lua_State *lua) const
{
	if (!_lua_self)
	{
		_lua_self = sol::reference(lua, sol::make_reference_userdata(lua, const_cast<Object*>(this)));
	}

	return _lua_self;
}

// somewhere in the same namespace as Object...
	/*
		When pointers or references to Objects are passed to Lua, use the
			Object's authoritative shared reference
	*/
	template<class Object_t, std::enable_if_t<std::is_base_of_v<Object, Object_t>, int> = 0>
	int sol_lua_push(lua_State* L, Object_t *object)    {return object ? sol::stack::push(L, object->as_lua(L)) : sol::stack::push(L, sol::lua_nil);}

	template<class Object_t, std::enable_if_t<std::is_base_of_v<Object, Object_t>, int> = 0>
	int sol_lua_push(lua_State* L, Object_t &object)    {return sol::stack::push(L, object.as_lua(L));}

My solution does nothing to ensure the Object lives as long as the Lua reference referring to it. You'll need some of this stuff for that.

EvanBalster avatar Nov 22 '25 02:11 EvanBalster