☰
sol2 多返回值实战:深入解析 `sol::variadic_results` 的构造、压栈机制与动态返回场景
2026/10/15 21:05:13 网站建设 项目流程
  • 游戏开发
  • 图形学

【免费下载链接】Piccolo

Piccolo (formerly Pilot) – mini game engine for games104

项目地址:https://gitcode.com/gh_mirrors/pi/Piccolo
点击查看免费下载

导读

sol::variadic_results是 sol2 Lua 绑定库提供的多返回值容器,它允许 C++ 函数在返回给 Lua 之前动态准备任意数量、任意类型的返回值,突破std::tuple只能返回固定类型组合、sol::as_returns只能返回同质容器元素的限制。本文以 sol2 3.3.0(随 Piccolo 引擎以engine/3rdparty/sol2-3.3.0方式内置)的官方 API 文档 variadic_results.rst 为主体,结合 variadic_results.hpp 源码实现、multi_results.cpp 官方示例与 variadics.cpp 运行时测试,完整讲解该类型的声明语义、五种构造途径、底层压栈原理,以及"按运行时条件返回不同数量/不同类型结果"的实战写法。读完本文,你将能在自己的 sol2 绑定代码中自如地使用sol::variadic_results实现动态多返回值。

一、sol::variadic_results是什么:先看官方文档定义

官方 API 文档 variadic_results.rst 对该类型给出了精炼的定义:

struct variadic_results : std::vector<object> { ... };

文档明确指出它的核心定位:

  • "push multiple disparate arguments into lua"——把多个彼此类型不同的参数一次性压入 Lua;
  • 它允许开发者在返回给 Lua 之前预先准备好多个返回值(prepare multiple returns before returning them into Lua);
  • 它派生自std::vector,因此可以完全像std::vector一样使用(push_back、insert、begin/end迭代器等);
  • 容器中的对象可以使用sol::object的各种构造函数及函数来添加(详见 object.rst)。

在 sol2 的 functions.rst 中,官方把"向 Lua 返回多个值"的途径归纳为两条:返回std::tuple,或使用sol::variadic_results。而 all-the-things.rst 则进一步点明其典型场景是"returning multiple differently-typed arguments"(返回多个不同类型的参数)。这意味着它解决的是"返回值类型异构 + 数量运行时才确定"这一类问题。

二、源码视角:继承关系与完整的构造函数族

文档给出的struct variadic_results : std::vector<object>只是示意图,真正的实现位于 variadic_results.hpp,它分成了两层:

// 第一层:带分配器模板参数的泛型基类 template <typename Al = typename std::allocator<object>> struct basic_variadic_results : public std::vector<object, Al> { using base_t = std::vector<object, Al>; basic_variadic_results(); // 默认构造 // 从 unsafe_function_result 批量接管返回值 basic_variadic_results(unsafe_function_result fr); // 从 protected_function_result 批量接管返回值 basic_variadic_results(protected_function_result fr); // 变参构造:直接转发给 std::vector 的区间/填充构造 template <typename Arg0, typename... Args, ...> basic_variadic_results(Arg0&& arg0, Args&&... args); }; // 第二层:面向用户的便捷别名 struct variadic_results : public basic_variadic_results<> { using base_t::basic_variadic_results; // 继承全部构造函数 };

从源码结构可以推断,设计上刻意保留了两层:basic_variadic_results<Al>允许用户通过自定义分配器Al控制sol::object的内存分配策略(默认是std::allocator<object>),而variadic_results则是使用默认分配器的即用类型。实际使用时直接写sol::variadic_results即可。

构造途径逐一解析

1. 默认构造——得到空容器,之后用push_back/insert填充:

sol::variadic_results values; values.push_back({ L, sol::in_place_type<int>, 42 });

2. 从unsafe_function_result/protected_function_result构造——这是非常有用的"返回值搬运"能力:当你在 C++ 里调用了一个 Lua 函数(得到unsafe_function_result或protected_function_result),想把它产生的一串返回值原样再转交出去时,可以直接构造:

sol::variadic_results(results_of_lua_call); // 自动 reserve + insert 所有返回值

其实现为this->reserve(fr.return_count()); this->insert(this->cend(), fr.begin(), fr.end());,即先按返回值数量预分配容量,再把结果迭代器范围内的每个元素插入。unsafe_function_result(见 unsafe_function_result.hpp)本身提供了begin/end迭代器与stack_proxy元素类型,因此这一转换是零成本语义上的"换壳"。

3. 变参转发构造——把任意参数直接转发给std::vector<object, Al>的对应构造函数。这正是官方测试tests/run_time/source/variadics.cpp中"迭代器区间构造"得以工作的入口:

lua.set_function("f", [](sol::variadic_args args) { return sol::variadic_results(args.cbegin(), args.cend()); });

该测试(第 178–202 行)验证了:Lua 侧执行v1, v2, v3 = f(1, 'bark', true)后,C++ 侧可分别取回int v1 == 1、std::string v2 == "bark"、bool v3 == true,且f(25, 82)返回两个值时v4, v5也能正确接收——返回值的数量完全由传入参数的数量动态决定。

值得注意的是,这个变参构造函数通过 SFINAE 禁用了三类实参:basic_variadic_results自身、function_result、protected_function_result(防止自拷贝与歧义),这些实参会走专门的重载或拷贝/移动路径。

三、元素怎么放进去:sol::object与sol::in_place构造

由于variadic_results的元素类型是sol::object,添加元素实际上就是构造sol::object。object.rst 列出了四种构造方式,其中与variadic_results配合最紧密的是两条in_place构造:

object(lua_State* L, in_place_t, T&& arg, Args&&... args); object(lua_State* L, in_place_type_t<T>, Args&&... args);
  • { L, sol::in_place, "awoo" }:按字面值类型就地构造一个对象;
  • { L, sol::in_place_type<int>, a }:显式指定目标类型T,把变量a转换为该类型后构造。

为什么需要传入L(lua_State*)?因为sol::object需要在指定的 Lua 状态上把值压栈、从栈上建立引用。而绑定到 Lua 的 C++ 函数里如何拿到当前的L?答案是透明参数sol::this_state(见 this_state.rst):它作为函数签名的一部分,调用时由 sol2 自动注入,Lua 侧无需感知。官方示例 multi_results.cpp 正是这一写法的标准范式:

lua.set_function( "multi_vars", [](int a, bool b, sol::this_state L) { sol::variadic_results values; values.push_back({ L, sol::in_place_type<int>, a }); values.push_back({ L, sol::in_place_type<bool>, b }); values.push_back({ L, sol::in_place, "awoo" }); return values; });

Lua 侧一次接收三个类型各不相同的返回值:

t, u, v = multi_vars(42, true) -- t == 42, u == true, v == "awoo"

官方示例随后在 C++ 侧断言sol_c_assert(t == 42); sol_c_assert(u); sol_c_assert(v == "awoo");,证明三种类型均被正确还原。

四、底层原理:stack::unqualified_pusher与is_container特化

variadic_results之所以能"一个返回值展开成多个 Lua 返回值",关键在于 variadic_results.hpp 末尾对 sol2 栈推送机制的定制:

template <> struct is_container<variadic_results> : std::false_type { }; namespace stack { template <> struct unqualified_pusher<variadic_results> { int push(lua_State* L, const variadic_results& r) { using base_t = basic_variadic_results<>; return stack::push(L, static_cast<const base_t&>(r)); } }; }

对应basic_variadic_results<Al>的 pusher 实现则逐元素压栈并累加数量:

int push(lua_State* L, const basic_variadic_results<Al>& e) { int p = 0; for (const auto& i : e) { p += stack::push(L, i); } return p; }

从源码结构可以梳理出两条关键机制:

  1. stack::unqualified_pusher决定"如何返回":sol2 约定,stack::push的返回值是该类型在 Lua 栈上占用的元素个数。variadic_results的 pusher 遍历容器内每个sol::object并逐个压栈,把p(压入的总数)作为返回值交给 Lua 运行时,Lua 便据此把栈顶这p个值全部当作函数的多重返回值。这正是"一个 C++ 返回值展开为 N 个 Lua 返回值"的底层实现。
  2. is_container<variadic_results> = false_type决定"不被当作普通容器":sol2 默认会把可迭代容器按"表/数组"方式转换,is_container特化为std::false_type后,sol2 就不会把variadic_results误当成std::vector<sol::object>那样的容器 usertype,而是走专门的 pusher 逻辑。这一对特化(variadic_results.hpp)是整个类型行为正确性的关键。

五、完整可运行示例:三种多返回值方案对比

官方 multi_results.cpp 一次性演示了 sol2 的全部三种多返回值手段,是理解variadic_results定位的最佳参照物:

方案适用场景局限
返回std::tuple返回值数量、类型在编译期固定无法按运行时条件改变数量
sol::as_returns(container)(见 as_returns.rst)返回值来自任意可迭代容器(std::vector、std::set、std::deque等)元素类型通常同质
sol::variadic_results动态数量 + 异构类型,需要精确控制每个返回值需手动push_back每个元素

as_returns的官方示例 as_returns.cpp 展示了容器方案:std::set<std::string> results {"arf", "bark", "woof"}经sol::as_returns(std::move(results))返回后,Lua 侧v1, v2, v3 = f()按序接收三个字符串。而as_returns.rst明确写着:"要更精细地控制返回值,请使用sol::variadic_results"(To have more control over the returns, usesol::variadic_results)——这正说明二者的分工:容器方案求便捷,variadic_results求精确与异构。

动态数量返回的完整示例

参考官方示例与测试,一个典型的"按条件返回不同数量和类型"的函数如下:

#define SOL_ALL_SAFETIES_ON 1 #include <sol/sol.hpp> int main() { sol::state lua; lua.open_libraries(sol::lib::base); lua.set_function("multi_vars", [](int a, bool b, sol::this_state L) { sol::variadic_results values; values.push_back({ L, sol::in_place_type<int>, a }); values.push_back({ L, sol::in_place_type<bool>, b }); values.push_back({ L, sol::in_place, "awoo" }); // 字符串按字面类型就地构造 return values; }); // Lua 侧多返回值接收 lua.script(R"( t, u, v = multi_vars(42, true) assert(t == 42 and u == true and v == "awoo") )"); return 0; }

再结合运行时测试中的写法,还可以用insert在容器任意位置追加元素(variadics.cpp):

lua.set_function("f", [](sol::this_state ts, bool maybe) { sol::variadic_results vr; if (maybe) { vr.push_back({ ts, sol::in_place, 1 }); vr.push_back({ ts, sol::in_place, 2 }); vr.insert(vr.cend(), { ts, sol::in_place, 3 }); // 用迭代器位置插入 return vr; // Lua 侧收到 3 个返回值 } else { vr.push_back({ ts, sol::in_place, "bark" }); vr.push_back({ ts, sol::in_place, "woof" }); vr.insert(vr.cend(), { ts, sol::in_place, "arf" }); vr.push_back({ ts, sol::in_place, "borf" }); return vr; // Lua 侧收到 4 个返回值 } });

该测试随后用lua.safe_script执行v1, v2, v3 = f(true)与v4, v5, v6, v7 = f(false),并逐一断言还原出的int/std::string值,完整验证了"同一函数、不同输入、不同数量的异构返回值"这一核心能力。

六、在 Piccolo 引擎中的落地场景

sol::variadic_results所属的 sol2 3.3.0 以第三方依赖形式内置于 Piccolo 引擎的 engine/3rdparty/sol2-3.3.0 目录(其 CMake 集成见 sol2 CMakeLists.txt)。引擎运行时确实在 Lua 脚本组件中直接使用了 sol2:见 lua_component.cpp,其中postLoadResource打开sol::lib::base基础库、通过m_lua_state.set_function注册set_float/get_bool/invoke等 C++ 函数,并在tick中逐帧执行m_lua_state.script(m_lua_script),m_lua_state的类型为sol::state(见 lua_component.h)。

在这种"C++ 引擎逻辑 ↔ Lua 脚本"双向绑定的架构里,variadic_results的典型用法是:C++ 侧的查询/计算函数一次向 Lua 脚本返回多个异构结果(例如同时返回整数 ID、布尔状态与字符串名称),或在 C++ 内部先拿到 Lua 函数的返回结果(unsafe_function_result/protected_function_result)后原样转发给另一处 Lua 调用。对于游戏对象属性查询这类"一问多答"的脚本接口,它能省去 Lua 侧手工组表/拆表的开销。

七、注意事项与最佳实践

  1. 元素一律是sol::object:variadic_results的元素类型固定为sol::object(由基类std::vector<object>决定),push 前需通过in_place/in_place_type或从其它 sol 引用类型构造好object。
  2. 必须持有lua_State*:构造元素需要当前状态,务必在函数签名中加入sol::this_state L透明参数;该参数由 sol2 注入,Lua 侧无需传值(官方示例注释明确说明 this_state 是 transparent argument)。
  3. 不要用裸std::vector<sol::object>替代:没有stack::unqualified_pusher特化时,容器会被 sol2 当作 Lua 表处理,无法产生多返回值;variadic_results的is_container = false_type特化正是为隔离这种默认行为而存在。
  4. 安全性开关:官方示例统一在 include 前定义#define SOL_ALL_SAFETIES_ON 1,开启后 sol2 会补充类型检查等安全机制(详见 functions.rst 的 function call safety 一节)。
  5. 选择正确的多返回值工具:编译期定长用std::tuple;同质容器展开用sol::as_returns;运行时变长、异构、需精细控制时,用sol::variadic_results——三者按需取用,不要一律套用。

总结

sol::variadic_results是 sol2 多返回值体系中灵活性最高的一环:它以std::vector<sol::object>为存储骨架,借助stack::unqualified_pusher的逐元素压栈与is_container的负向特化,实现了"动态数量 + 异构类型"的 Lua 多返回值语义;配合sol::in_place构造与sol::this_state透明参数,可以写出既精确又直观的绑定代码。官方文档、头文件实现、multi_results.cpp 示例与 variadics.cpp 测试互为印证,构成了理解与使用该 API 的完整证据链。在 Piccolo 这类"C++ 引擎 + Lua 脚本"混合架构中,它正是连接两侧异构数据流的实用桥梁。

  • 游戏开发
  • 图形学

【免费下载链接】Piccolo

Piccolo (formerly Pilot) – mini game engine for games104

项目地址:https://gitcode.com/gh_mirrors/pi/Piccolo
点击查看免费下载
上一篇:Claude Desktop Linux 安装指南:Fedora/Arch 用户的四条路径与避坑要点
下一篇:TV-Bro:把遥控器当鼠标用的免费开源电视浏览器,3 步上手

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询