template<typename ... Args>
bool run_script(int id, Args && ... args) {
const int params = sizeof...(args);
_load(id);
push(std::forward<Args>(args)...);
return _run(params);
}
Make sure you are not overflowing the lua stack. From the Lua 5.3 Manual,
4.2 – Stack Size
When you interact with the Lua API, you are responsible for ensuring consistency. In particular, you are responsible for controlling stack overflow. You can use the function lua_checkstack
to ensure that the stack has enough space for pushing new elements.
Whenever Lua calls C, it ensures that the stack has space for at least LUA_MINSTACK
extra slots. LUA_MINSTACK
is defined as 20, so that usually you do not have to worry about stack space unless your code has loops pushing elements onto the stack.
When you call a Lua function without a fixed number of results (see lua_call
), Lua ensures that the stack has enough space for all results, but it does not ensure any extra space. So, before pushing anything in the stack after such a call you should use lua_checkstack
.
For functions prefixed with _
, are these meant to be apart of the public interface? If not, hide them through a named namespace (impl
, detail
) to indicate they shouldn't be called.
Each of your _push
functions works only with the hard-coded L
lua_state*
. Consider refactoring the state into an argument.
Ensure you are using a resource manager for L
(the lua_state
) so that it doesn't leak. Use the C++ type system until you have to interact at a boundary (like a C-API).
When recursively pack expanding, you have to consider what can be passed. You handled the cases where the pack contains arguments. What happens if the pack doesn't contain arguments? As @prettyfly mentioned, define the behavior for that case.
Looking at "Parameter Pack", we can also expand through a braced-init-list ({}
). A braced-init-list guarantees left-to-right evaluation of the parameter pack.
Note - The reference page for "Parameter Pack" uses int dummy[sizeof...(Ts)]
for the pack expansion. Zero-length arrays are not standard and are only supported through compiler extensions. Ensure the resulting expansion has a size of 1 by padding the resulting array. - end note
namespace detail {
void push(LuaState& state, int arg) {
lua_pushinteger(*state, arg);
}
// other forwarded types to push
}
template <typename Arg>
void push(LuaState& state, Arg&& arg) {
return detail::push(state, std::forward<Arg>(arg));
}
template <typename... Args>
void push_all(LuaState& state, Args&&... args) {
std::initializer_list<int>{(push(state, std::forward<Args>(args)), 0)...};
}
If you pass zero arguments, nothing is evaluated. If you pass in arguments, they are each forwarded to the single argument helper. You should be aware of the following with this method:
braced-init-list expansion will produce
warning: expression result unused [-Wunused-value]
To discard the expression result, §5.2.9/4 in the C++ Standard allows us to cast the result to void
.
(void) std::initializer_list<int>{(push(state, std::forward<Args>(args)), 0)...};
^^^^^^
The comma operator is vulnerable to user-defined-types that overload operator,
. You can guard against such an evil practice in two ways
a. Pass void()
to operator,
(void) std::initializer_list<int>{(push(state, std::forward<Args>(args)), void(), 0)...};
^^^^^^
Functions cannot be defined to take arguments of type void
. If a void
type is passed as an argument to a template, substitution failure will occur, and the compiler reverts to the built-in comma operator.
b. Discard the result
(void) std::initializer_list<int>{((void) push(state, std::forward<Args>(args)), 0)...};
^^^^^^
In c++17, we'll have fold-expressions, so we'll be able to just write
((void)(push(state, args), ...); // or
(push(state, args), void(), ...);
Each expanded argument still needs to be guarded against user-defined data types overloading operator,
.
Rather than specializing for every type,
void _push(int arg) {
lua_pushinteger(L, arg);
}
Consider specializing on constrained types through tag dispatching (tutorial here). Tag dispatching leverages the C++ language and compiler to select the appropriate candidate function through tags. <type_traits>
provides the tools you'll need to test for most of the types. For missing types (like is_string<T>
), you'll have to roll your own or pull from Boost or another metaprogramming library. There are other metaprogramming techniques that can be used as well until C++ implements the Concepts proposal in a future standard (hopefully in C++20).