I started reading the GameProgrammingPatterns
book and wanted to implement the command pattern.
namespace bebop::patterns
{
struct Command
{
using Fn = void (*)(void*); // Command return type is void
Fn exec; // The function to be executed when execute() is called
void* m_ctx; // The context for the function
void execute() const { exec(m_ctx); }
};
template <typename T, typename Method, typename... Args>
struct CommandImpl
{
CommandImpl(T* obj, Method method, Args&&... args)
: m_ctx{obj, method, std::make_tuple(std::forward<Args>(args)...)}
{
m_command = Command{&trampoline, &m_ctx};
}
void execute() const { m_command.execute(); }
Command command() const { return m_command; }
private:
struct Context
{
T* obj;
Method method;
std::tuple<Args...> args;
};
Context m_ctx;
static void trampoline(void* raw)
{
auto* m_ctx = static_cast<Context*>(raw);
std::apply(
[m_ctx](auto&&... unpacked_args)
{
(m_ctx->obj->*m_ctx->method)(
std::forward<decltype(unpacked_args)>(unpacked_args)...);
},
m_ctx->args);
}
Command m_command;
};
// Deduction guide
template <typename T, typename Method, typename... Args>
auto make_command(T* obj, Method method, Args&&... args)
{
return CommandImpl<T, Method, std::decay_t<Args>...>(
obj, method, std::forward<Args>(args)...);
}
} // namespace bebop::patterns
Decided to implement without virtual functions because we all know that virtual functions add runtime overhead.std::function
is adding overhead too, so the only right way to statically do this, without even heap overhead is this.
- To generalize the command function we shall have a function pointer, usually when executing a command action we are not interested in the return result, but the command action function may consume some parameters. So to generalize it we shall have a
void(*)(void*)
function pointer. - To have somehow any context about the function we must also store a
void* ctx
to store the context of the function somewhere.
Now, its gonna be an overhead for an engineer to create the command pointer, the context to create command. So to have some sugar coding lets add a template make_command
which receives the pointer to the object, the action of the object, and the parameters that action shall take. But to implement this, we cannot use any captures , std::function
or etc... So we must somehow write a trampoline for our case. Thats why templated CommandImpl is used, to create a right command with the trampoline method and right context(with the arguments injected as a tuple). And the templated make_command
just returns that implementation.
The usage:
#include <iostream>
#include <memory>
#include <unordered_map>
#include <utility>
#include "Command.hpp"
struct Player
{
void moveUp() { std::cout << "UP\n"; }
void moveDown() { std::cout << "DOWN\n"; }
void moveRight() { std::cout << "RIGHT\n"; }
void moveLeft() { std::cout << "LEFT\n"; }
void shoot(int n) { std::cout << "Shoot " << n << "\n"; }
};
struct InputHandler
{
void bind(char c, bebop::patterns::Command cmd) { bindings[c] = cmd; }
bebop::patterns::Command* getCommand(char c)
{
auto it = bindings.find(c);
return it != bindings.end() ? &it->second : nullptr;
}
template <typename CmdImpl>
void bindOwned(char c, CmdImpl&& impl)
{
using ImplType = std::decay_t<CmdImpl>;
auto implPtr = std::make_shared<ImplType>(std::forward<CmdImpl>(impl));
// Extract the Command before erasing type
bebop::patterns::Command cmd = implPtr->command();
// Store lifetime-managed pointer
storage.emplace_back(std::move(implPtr));
// Bind command (safe — the context pointer is still valid inside
// CommandImpl)
bind(c, cmd);
}
private:
std::unordered_map<char, bebop::patterns::Command> bindings;
std::vector<std::shared_ptr<void>> storage; // type-erased ownership
};
int main()
{
Player player;
InputHandler handler;
handler.bindOwned('w',
bebop::patterns::make_command(&player, &Player::moveUp));
handler.bindOwned(
'v', bebop::patterns::make_command(&player, &Player::shoot, 5));
auto m_command = bebop::patterns::make_command(&player, &Player::shoot, 10);
m_command.execute();
std::string s;
while (std::cin >> s)
{
if (s.size() != 1)
{
continue;
}
if (auto* cmd = handler.getCommand(s[0]))
cmd->execute();
}
return 0;
}
Basically to create a command, as mentioned you shall use make_command
templated method, or it's your responsibility to create a context and a method yourself.
It's going to be a little overhead for input handler for instance to store the commands, but there will always be one input handler in the whole game... but many commands.
What do you think? Is this design good? Or are there any other ways to do this, without virtual overhead, lambda captures and std::function
?
command
that is mapped to achar
or is it fixed after initialization? \$\endgroup\$