This is over-engineered and way too complicated. If you read GameProgrammingPatterns you eventually find that they recommend command be called with the context that is required with it, rather than trying to attach all local context, this obviates a lot of the design decisions made here on it's own, but you really should have just used std::function from the start with the design mentality you had here.
Additionally
there's only so many keys available, and thus no point in using an unordered map (which below a certain amount of values can be slower than small vector anyway).
you claim that you're not using std::function because of the performance penalties, but then go a head and just use shared_ptr everywhere in order to re-invent it.
Shared pointers should only be used when you have asynchronous lifetimes. As in, when you create object A, you cannot deterministically figure out how long A should be alive for, ie, there's no scope {} that determines A's lifetime from start to end. This happens only in async/concurrent/multithreaded environments.
All of this extra things you've added has effectively negated any performance gains you think you've had by not using std::function, and made the code complicated for no reason as a result.
Your use of std::shared_ptr is effectively what a std::function would have to do but worse in virtually all aspects. It's the reason you have to type erase, you're getting no benefits performance or organizationally from doing this, and you're still getting dynamic allocation.
I will demonstrate how much simpler this can be. I'll be assuming you have access to GLFW (because this is for game programming and meant to be used for real input handling), though the concepts I'm using here are the same for other windowing frameworks. I don't even need to use std::function in this one to get the same capabilities you have, and don't allocate on the heap at all (at least from the code written, GLFW/OpenGL might behind the scenes), not that you should avoid heap allocations just because, the stack itself is an allocation after all, just known ahead of time. Note I've also not ran this, this is just a quick example.
#include <glad/gl.h>
#include <GLFW/glfw3.h>
#include <array>
//see https://www.glfw.org/docs/3.3/group__keys.html
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 Context{
Player player;
InputHandler input_handler;
}
using CommandFunction = void(Context& context);
struct InputHandler{
//zero initialize the space for all keys.
std::array<CommandFunction, GLFW_KEY_LAST + 1> commands = {};
};
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods){
//Hypothetically, actions, and mods may be useful, but we will leave that up as an excercise for the reader.
Context* context = glfwGetWindowUserPointer(window);
//check if we actually filled the given command in for the given key.
if(context->input_handler.commands[key] != nullptr && action = GLFW_PRESS){
context->input_handler.commands[key](*context);
}
}
int main(){
//Taken from https://www.glfw.org/docs/3.3/quick_guide.html
if (!glfwInit()){
// Initialization failed
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
//just creating a window that can use input.
GLFWwindow* window = glfwCreateWindow(640, 480, "My Title", NULL, NULL);
if (!window){
// Window or OpenGL context creation failed
}
glfwMakeContextCurrent(window);
gladLoadGL(glfwGetProcAddress);
Context context;
//our callback can now access context.
glfwSetWindowUserPointer(window, &context);
//lambda functions with out captures are valid free functions
context.input_handler[GLFW_KEY_UP] = [](Context& context){
context.player.moveUp();
};
context.input_handler[GLFW_KEY_DOWN] = [](Context& context){
context.player.moveDown();
};
context.input_handler[GLFW_KEY_RIGHT] = [](Context& context){
context.player.moveRight();
};
context.input_handler[GLFW_KEY_LEFT] = [](Context& context){
context.player.moveLeft();
};
context.input_handler[GLFW_KEY_SPACE] = [](Context& context){
context.player.shoot(10);
};
context.input_handler[GLFW_KEY_Q] = [](Context& context){
context.player.shoot(5);
};
//poll events will process input events, and our callback will automatically get called, dispatching what was in input_handler
// (callbacks are processed in this thread).
while (!glfwWindowShouldClose(window)){
int width, height;
glfwGetFramebufferSize(window, &width, &height);
//we don't really care what opengl is doing here, all we need is a window to consume key inputs.
glViewport(0, 0, width, height);
glClear(GL_COLOR_BUFFER_BIT);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
}
In other situations, you may actually need to capture, and typically because of that, you'd want to change the commands array into an array of std::function<void(Context&)> to deal with those instances. And you may want to attach multiple inputs per key, or have keys mapped to key combinations instead of specific key presses, which may or may not necessitate using a different container than std::array.