From 4fdb843090f00a9695811103bc255bb4ee625f5e Mon Sep 17 00:00:00 2001 From: Fromant Date: Fri, 12 Dec 2025 21:40:34 +0300 Subject: [PATCH 01/15] first version - wrapper with int only parameters and return type --- CMakeLists.txt | 7 ++++++ Engine.hpp | 2 ++ Wrapper.hpp | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.cpp | 15 +++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 Engine.hpp create mode 100644 Wrapper.hpp create mode 100644 main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5a1458e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.14) +project(CommandEngine) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(CommandEngine main.cpp) \ No newline at end of file diff --git a/Engine.hpp b/Engine.hpp new file mode 100644 index 0000000..99ddf32 --- /dev/null +++ b/Engine.hpp @@ -0,0 +1,2 @@ + +#pragma once diff --git a/Wrapper.hpp b/Wrapper.hpp new file mode 100644 index 0000000..879d70c --- /dev/null +++ b/Wrapper.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include + +// T is a object type +// Ret is a function's return type +// Args is a function's parameters types +template +class Wrapper { + static constexpr size_t ARG_COUNT = sizeof...(Args); + + using ArgMap = std::unordered_map; + using ArgList = std::vector>; + using Func = int (T::*)(Args...); + + T* const _obj; + const Func _func; + + const ArgList argNames; + + template + Ret call_with_indices(const std::array& args, std::index_sequence) { + return (_obj->*_func)(args[Indices]...); + } + +public: + Wrapper(T* const object, Func function, const ArgList& argList) : + _obj(object), _func(function), argNames(argList) { + if (argList.size() != ARG_COUNT) { + throw std::invalid_argument("Wrong number of arguments"); + } + } + + Ret execute(const ArgList& list) { + if (list.size() > ARG_COUNT) { + throw std::invalid_argument("Too many arguments"); + } + std::array args; + + for (size_t i = 0; i < ARG_COUNT; i++) { + const auto& [name, def] = argNames[i]; + auto it = std::find_if(list.begin(), list.end(), [&name](const std::pair& arg) { + return arg.first == name; + }); + + if (it == list.end()) { + args[i] = def; + } + else { + args[i] = it->second; + } + } + + return call_with_indices(args, std::make_index_sequence()); + } +}; diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..bd2ec03 --- /dev/null +++ b/main.cpp @@ -0,0 +1,15 @@ +#include "Wrapper.hpp" + +struct A { + int foo(int a, int b) { + return b - a; + } +}; + +int main() { + A a; + Wrapper t(&a, &A::foo, {{"a", 12}, {"b", 11}}); + return t.execute({ + {"a", 5} + }); +} From 32d2e7a0772fbdb3eefb6c6d446d7d4af7b836ee Mon Sep 17 00:00:00 2001 From: Fromant Date: Fri, 12 Dec 2025 21:54:50 +0300 Subject: [PATCH 02/15] add type erasure through std::any Some type-safety included --- Wrapper.hpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Wrapper.hpp b/Wrapper.hpp index 879d70c..74ec0f4 100644 --- a/Wrapper.hpp +++ b/Wrapper.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include #include // T is a object type @@ -12,18 +14,19 @@ template class Wrapper { static constexpr size_t ARG_COUNT = sizeof...(Args); - using ArgMap = std::unordered_map; - using ArgList = std::vector>; - using Func = int (T::*)(Args...); + using ArgMap = std::unordered_map; + using ArgList = std::vector>; + using Func = Ret (T::*)(Args...); T* const _obj; const Func _func; const ArgList argNames; + std::array argTypes = { typeid(Args)... }; template - Ret call_with_indices(const std::array& args, std::index_sequence) { - return (_obj->*_func)(args[Indices]...); + Ret call_with_indices(const std::array& args, std::index_sequence) { + return (_obj->*_func)(std::any_cast(args[Indices])...); } public: @@ -38,11 +41,11 @@ class Wrapper { if (list.size() > ARG_COUNT) { throw std::invalid_argument("Too many arguments"); } - std::array args; + std::array args; for (size_t i = 0; i < ARG_COUNT; i++) { const auto& [name, def] = argNames[i]; - auto it = std::find_if(list.begin(), list.end(), [&name](const std::pair& arg) { + auto it = std::find_if(list.begin(), list.end(), [&name](const std::pair& arg) { return arg.first == name; }); @@ -50,6 +53,9 @@ class Wrapper { args[i] = def; } else { + if (it->second.type() != argTypes[i]) { + throw std::invalid_argument("Type mismatch for argument: " + name); + } args[i] = it->second; } } From 114cbb2248cea7e15f4cf202a5fee5e41deec598 Mon Sep 17 00:00:00 2001 From: Fromant Date: Fri, 12 Dec 2025 22:02:44 +0300 Subject: [PATCH 03/15] basic engine --- Engine.hpp | 33 ++++++++++++++++++++++++++++++++- Wrapper.hpp | 13 ++++++++----- main.cpp | 14 +++++++++++--- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/Engine.hpp b/Engine.hpp index 99ddf32..946230c 100644 --- a/Engine.hpp +++ b/Engine.hpp @@ -1,2 +1,33 @@ - #pragma once + +#include +#include +#include +#include +#include +#include + +#include "Wrapper.hpp" + +class Engine { +public: + template + void register_command(const Wrapper* wrapper, const std::string& command_name) { + auto executor = [wrapper](const ArgList& args) -> std::any { + return wrapper->execute(args); + }; + command_executors[command_name] = executor; + } + + std::any execute(const std::string& command_name, const ArgList& args) { + auto it = command_executors.find(command_name); + if (it == command_executors.end()) { + throw std::invalid_argument("Command not found: " + command_name); + } + + return it->second(args); + } + +private: + std::unordered_map> command_executors; +}; diff --git a/Wrapper.hpp b/Wrapper.hpp index 74ec0f4..22f615e 100644 --- a/Wrapper.hpp +++ b/Wrapper.hpp @@ -7,6 +7,11 @@ #include #include + +using ArgMap = std::unordered_map; +using ArgList = std::vector>; + + // T is a object type // Ret is a function's return type // Args is a function's parameters types @@ -14,18 +19,16 @@ template class Wrapper { static constexpr size_t ARG_COUNT = sizeof...(Args); - using ArgMap = std::unordered_map; - using ArgList = std::vector>; using Func = Ret (T::*)(Args...); T* const _obj; const Func _func; const ArgList argNames; - std::array argTypes = { typeid(Args)... }; + std::array argTypes = {typeid(Args)...}; template - Ret call_with_indices(const std::array& args, std::index_sequence) { + Ret call_with_indices(const std::array& args, std::index_sequence) const { return (_obj->*_func)(std::any_cast(args[Indices])...); } @@ -37,7 +40,7 @@ class Wrapper { } } - Ret execute(const ArgList& list) { + Ret execute(const ArgList& list) const { if (list.size() > ARG_COUNT) { throw std::invalid_argument("Too many arguments"); } diff --git a/main.cpp b/main.cpp index bd2ec03..28d6a4a 100644 --- a/main.cpp +++ b/main.cpp @@ -1,3 +1,6 @@ +#include + +#include "Engine.hpp" #include "Wrapper.hpp" struct A { @@ -8,8 +11,13 @@ struct A { int main() { A a; - Wrapper t(&a, &A::foo, {{"a", 12}, {"b", 11}}); - return t.execute({ + Wrapper t(&a, &A::foo, {{"a", 12}, {"b", 11}}); + std::cout << t.execute({ {"a", 5} - }); + }) << std::endl; + + Engine e; + e.register_command(&t, "t"); + + std::cout << std::any_cast(e.execute("t", {{"b", 16}})) << std::endl; } From d982ae598f0f956a1c2a225f0d3646fe9d86687f Mon Sep 17 00:00:00 2001 From: Fromant Date: Fri, 12 Dec 2025 22:38:02 +0300 Subject: [PATCH 04/15] make engine and wrapper memory safe add convenient register_command overloads in Engine.hpp --- Engine.hpp | 34 +++++++++++++++++++---------- Wrapper.hpp | 58 ++++++++++++++++++++++++++++++++++++++----------- WrapperBase.hpp | 12 ++++++++++ main.cpp | 9 ++++---- 4 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 WrapperBase.hpp diff --git a/Engine.hpp b/Engine.hpp index 946230c..3a616f6 100644 --- a/Engine.hpp +++ b/Engine.hpp @@ -2,32 +2,44 @@ #include #include +#include #include #include #include -#include #include "Wrapper.hpp" class Engine { public: template - void register_command(const Wrapper* wrapper, const std::string& command_name) { - auto executor = [wrapper](const ArgList& args) -> std::any { - return wrapper->execute(args); - }; - command_executors[command_name] = executor; + void register_command(const std::string& name, const Wrapper& wrapper) { + wrappers.emplace(name, std::make_unique>(wrapper)); } - std::any execute(const std::string& command_name, const ArgList& args) { - auto it = command_executors.find(command_name); - if (it == command_executors.end()) { + template + void register_command(const std::string& name, CtorArgs&&... args) { + wrappers.emplace(name, std::make_unique>(std::forward(args)...)); + } + + void register_command(const std::string& name, std::unique_ptr&& wrapper) { + wrappers.emplace(name, std::move(wrapper)); + } + + template + void register_command(const std::string& name, Wrapper&& wrapper) { + wrappers.emplace(name, std::make_unique>(std::move(wrapper))); + } + + + std::any execute(const std::string& command_name, const WrapperBase::ArgList& args) { + auto it = wrappers.find(command_name); + if (it == wrappers.end()) { throw std::invalid_argument("Command not found: " + command_name); } - return it->second(args); + return it->second->execute(args); } private: - std::unordered_map> command_executors; + std::unordered_map> wrappers; }; diff --git a/Wrapper.hpp b/Wrapper.hpp index 22f615e..a931d00 100644 --- a/Wrapper.hpp +++ b/Wrapper.hpp @@ -7,40 +7,66 @@ #include #include - -using ArgMap = std::unordered_map; -using ArgList = std::vector>; +#include "WrapperBase.hpp" // T is a object type // Ret is a function's return type // Args is a function's parameters types template -class Wrapper { +class Wrapper : public WrapperBase { static constexpr size_t ARG_COUNT = sizeof...(Args); + using ArgMap = std::unordered_map; using Func = Ret (T::*)(Args...); - T* const _obj; - const Func _func; + std::unique_ptr _obj; + Func _func; - const ArgList argNames; + ArgList argNames; std::array argTypes = {typeid(Args)...}; template - Ret call_with_indices(const std::array& args, std::index_sequence) const { - return (_obj->*_func)(std::any_cast(args[Indices])...); + std::any call_with_indices(const std::array& args, std::index_sequence) const { + return (*_obj.*_func)(std::any_cast(args[Indices])...); } public: - Wrapper(T* const object, Func function, const ArgList& argList) : - _obj(object), _func(function), argNames(argList) { + Wrapper(const T& object, Func function, const ArgList& argList) : + _obj(std::make_unique(object)), _func(function), argNames(argList) { if (argList.size() != ARG_COUNT) { throw std::invalid_argument("Wrong number of arguments"); } } - Ret execute(const ArgList& list) const { + Wrapper(const Wrapper& other): _obj(std::make_unique(*other._obj)), _func(other._func), + argNames(other.argNames) {} + + Wrapper(Wrapper&& other) noexcept : _obj(std::move(other._obj)), _func(std::move(other._func)), + argNames(std::move(other.argNames)) {} + + // Copy assignment + Wrapper& operator=(const Wrapper& other) { + if (this != &other) { + _obj = std::make_unique(*other._obj); + _func = other._func; + argNames = other.argNames; + } + return *this; + } + + // Move assignment + Wrapper& operator=(Wrapper&& other) noexcept { + if (this != &other) { + _obj = std::move(other._obj); + _func = other._func; + argNames = std::move(other.argNames); + } + return *this; + } + + + std::any execute(const ArgList& list) const override { if (list.size() > ARG_COUNT) { throw std::invalid_argument("Too many arguments"); } @@ -63,6 +89,12 @@ class Wrapper { } } - return call_with_indices(args, std::make_index_sequence()); + // special case for Ret == void + if constexpr (std::is_same_v) { + call_with_indices(args, std::make_index_sequence{}); + return {}; + } + + return call_with_indices(args, std::make_index_sequence{}); } }; diff --git a/WrapperBase.hpp b/WrapperBase.hpp new file mode 100644 index 0000000..2ae1755 --- /dev/null +++ b/WrapperBase.hpp @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include + +class WrapperBase { +public: + using ArgList = std::vector>; + + virtual std::any execute(const ArgList& args) const = 0; + virtual ~WrapperBase() = default; +}; \ No newline at end of file diff --git a/main.cpp b/main.cpp index 28d6a4a..59cd615 100644 --- a/main.cpp +++ b/main.cpp @@ -11,13 +11,14 @@ struct A { int main() { A a; - Wrapper t(&a, &A::foo, {{"a", 12}, {"b", 11}}); - std::cout << t.execute({ + Wrapper t(a, &A::foo, {{"a", 12}, {"b", 11}}); + std::cout << std::any_cast(t.execute({ {"a", 5} - }) << std::endl; + })) << std::endl; Engine e; - e.register_command(&t, "t"); + e.register_command("t", t); + e.register_command("t", std::move(t)); std::cout << std::any_cast(e.execute("t", {{"b", 16}})) << std::endl; } From 9c0173474c7311fd623f1cbd01de3bf7f837a593 Mon Sep 17 00:00:00 2001 From: Fromant Date: Fri, 12 Dec 2025 23:32:07 +0300 Subject: [PATCH 05/15] move everything to src/ fix register_command with params of Wrapper ctor Wrapper make void function handling more elegant --- CMakeLists.txt | 7 +++++-- main.cpp | 19 +++++++------------ Engine.hpp => src/Engine.hpp | 11 ++++++++--- Wrapper.hpp => src/Wrapper.hpp | 23 +++++++++++++---------- WrapperBase.hpp => src/WrapperBase.hpp | 0 tests/CMakeLists.txt | 19 +++++++++++++++++++ 6 files changed, 52 insertions(+), 27 deletions(-) rename Engine.hpp => src/Engine.hpp (83%) rename Wrapper.hpp => src/Wrapper.hpp (78%) rename WrapperBase.hpp => src/WrapperBase.hpp (100%) create mode 100644 tests/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a1458e..6b3cd48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,10 @@ cmake_minimum_required(VERSION 3.14) -project(CommandEngine) +project(EngineWrapper) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -add_executable(CommandEngine main.cpp) \ No newline at end of file +add_executable(EngineWrapper main.cpp) +target_include_directories(EngineWrapper PRIVATE src) + +add_subdirectory(tests) diff --git a/main.cpp b/main.cpp index 59cd615..02e8008 100644 --- a/main.cpp +++ b/main.cpp @@ -1,24 +1,19 @@ #include #include "Engine.hpp" -#include "Wrapper.hpp" +#include "src/Wrapper.hpp" struct A { - int foo(int a, int b) { - return b - a; - } + int f(int a, int b) { return a + b; } }; int main() { - A a; - Wrapper t(a, &A::foo, {{"a", 12}, {"b", 11}}); - std::cout << std::any_cast(t.execute({ - {"a", 5} - })) << std::endl; + A obj; + Wrapper w1(obj, &A::f, {{"a",0},{"b",0}}); // non-const member Engine e; - e.register_command("t", t); - e.register_command("t", std::move(t)); + e.register_command("w1", w1); + e.register_command("w2", std::move(w1)); - std::cout << std::any_cast(e.execute("t", {{"b", 16}})) << std::endl; + std::cout << std::any_cast(e.execute("w1", {{"b", 16}})) << std::endl; } diff --git a/Engine.hpp b/src/Engine.hpp similarity index 83% rename from Engine.hpp rename to src/Engine.hpp index 3a616f6..d28eea0 100644 --- a/Engine.hpp +++ b/src/Engine.hpp @@ -16,9 +16,14 @@ class Engine { wrappers.emplace(name, std::make_unique>(wrapper)); } - template - void register_command(const std::string& name, CtorArgs&&... args) { - wrappers.emplace(name, std::make_unique>(std::forward(args)...)); + template + void register_command( + const std::string& name, + const T& obj, + Ret(T::*func)(Args...), + const WrapperBase::ArgList& argList + ) { + wrappers.emplace(name, std::make_unique>(obj, func, argList)); } void register_command(const std::string& name, std::unique_ptr&& wrapper) { diff --git a/Wrapper.hpp b/src/Wrapper.hpp similarity index 78% rename from Wrapper.hpp rename to src/Wrapper.hpp index a931d00..d550157 100644 --- a/Wrapper.hpp +++ b/src/Wrapper.hpp @@ -26,9 +26,18 @@ class Wrapper : public WrapperBase { ArgList argNames; std::array argTypes = {typeid(Args)...}; - template - std::any call_with_indices(const std::array& args, std::index_sequence) const { - return (*_obj.*_func)(std::any_cast(args[Indices])...); + template + std::enable_if_t, std::any> + invoke_function(const std::array& args, std::index_sequence) const { + return ((*_obj).*_func)(std::any_cast(args[Indices])...); + } + + // Helper for void return + template + std::enable_if_t, std::any> + invoke_function(const std::array& args, std::index_sequence) const { + ((*_obj).*_func)(std::any_cast(args[Indices])...); + return {}; // or std::any{} } public: @@ -89,12 +98,6 @@ class Wrapper : public WrapperBase { } } - // special case for Ret == void - if constexpr (std::is_same_v) { - call_with_indices(args, std::make_index_sequence{}); - return {}; - } - - return call_with_indices(args, std::make_index_sequence{}); + return invoke_function(args, std::make_index_sequence{}); } }; diff --git a/WrapperBase.hpp b/src/WrapperBase.hpp similarity index 100% rename from WrapperBase.hpp rename to src/WrapperBase.hpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..4470be7 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.14) +project(EngineWrapperTests) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) +FetchContent_MakeAvailable(googletest) + +file(GLOB TEST_SOURCES "*.cpp") +add_executable(EngineWrapperTests ${TEST_SOURCES}) +target_link_libraries(EngineWrapperTests gtest gtest_main) +target_include_directories(EngineWrapperTests PRIVATE ../src) + From 945f7f084013d5e0806cee59056314fb206e214e Mon Sep 17 00:00:00 2001 From: Fromant Date: Fri, 12 Dec 2025 23:33:24 +0300 Subject: [PATCH 06/15] format --- src/Wrapper.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Wrapper.hpp b/src/Wrapper.hpp index d550157..54b2789 100644 --- a/src/Wrapper.hpp +++ b/src/Wrapper.hpp @@ -36,7 +36,7 @@ class Wrapper : public WrapperBase { template std::enable_if_t, std::any> invoke_function(const std::array& args, std::index_sequence) const { - ((*_obj).*_func)(std::any_cast(args[Indices])...); + (*_obj.*_func)(std::any_cast(args[Indices])...); return {}; // or std::any{} } From 7410b798f2c1c1f271ee8c4d47b6d4ef6435bd02 Mon Sep 17 00:00:00 2001 From: Fromant Date: Fri, 12 Dec 2025 23:59:17 +0300 Subject: [PATCH 07/15] easier registration, support subject being uniq ptr, raw ptr, shared ptr or const ref --- src/Engine.hpp | 13 ++----------- src/Wrapper.hpp | 52 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/Engine.hpp b/src/Engine.hpp index d28eea0..8da4eab 100644 --- a/src/Engine.hpp +++ b/src/Engine.hpp @@ -16,25 +16,16 @@ class Engine { wrappers.emplace(name, std::make_unique>(wrapper)); } - template + template void register_command( const std::string& name, - const T& obj, + Obj obj, Ret(T::*func)(Args...), const WrapperBase::ArgList& argList ) { wrappers.emplace(name, std::make_unique>(obj, func, argList)); } - void register_command(const std::string& name, std::unique_ptr&& wrapper) { - wrappers.emplace(name, std::move(wrapper)); - } - - template - void register_command(const std::string& name, Wrapper&& wrapper) { - wrappers.emplace(name, std::make_unique>(std::move(wrapper))); - } - std::any execute(const std::string& command_name, const WrapperBase::ArgList& args) { auto it = wrappers.find(command_name); diff --git a/src/Wrapper.hpp b/src/Wrapper.hpp index 54b2789..9e04d4a 100644 --- a/src/Wrapper.hpp +++ b/src/Wrapper.hpp @@ -20,54 +20,78 @@ class Wrapper : public WrapperBase { using ArgMap = std::unordered_map; using Func = Ret (T::*)(Args...); - std::unique_ptr _obj; Func _func; + std::function _get_obj; + ArgList argNames; std::array argTypes = {typeid(Args)...}; template std::enable_if_t, std::any> invoke_function(const std::array& args, std::index_sequence) const { - return ((*_obj).*_func)(std::any_cast(args[Indices])...); + return (_get_obj().*_func)(std::any_cast(args[Indices])...); } // Helper for void return template std::enable_if_t, std::any> invoke_function(const std::array& args, std::index_sequence) const { - (*_obj.*_func)(std::any_cast(args[Indices])...); - return {}; // or std::any{} + (_get_obj().*_func)(std::any_cast(args[Indices])...); + return {}; } public: - Wrapper(const T& object, Func function, const ArgList& argList) : - _obj(std::make_unique(object)), _func(function), argNames(argList) { - if (argList.size() != ARG_COUNT) { + Wrapper(T* obj, Func func, const ArgList& argList): + _get_obj([obj]() -> T& { return *obj; }), + _func(func), + argNames(argList) { + if (argList.size() != ARG_COUNT) + throw std::invalid_argument("Wrong number of arguments"); + } + + Wrapper(const T& obj, Func func, const ArgList& argList): + _get_obj([owned = T(obj)]() mutable -> T& { return owned; }), + _func(func), + argNames(argList) { + if (argList.size() != ARG_COUNT) + throw std::invalid_argument("Wrong number of arguments"); + } + + Wrapper(std::shared_ptr obj, Func func, const ArgList& argList): + _get_obj([obj = std::move(obj)]() -> T& { return *obj; }), + _func(func) + , argNames(argList) { + if (argList.size() != ARG_COUNT) + throw std::invalid_argument("Wrong number of arguments"); + } + + Wrapper(std::unique_ptr obj, Func func, const ArgList& argList): + _get_obj([obj = std::move(obj)]() -> T& { return *obj; }), + _func(func), + argNames(argList) { + if (argList.size() != ARG_COUNT) throw std::invalid_argument("Wrong number of arguments"); - } } - Wrapper(const Wrapper& other): _obj(std::make_unique(*other._obj)), _func(other._func), + Wrapper(const Wrapper& other): _get_obj(other._get_obj), _func(other._func), argNames(other.argNames) {} - Wrapper(Wrapper&& other) noexcept : _obj(std::move(other._obj)), _func(std::move(other._func)), + Wrapper(Wrapper&& other) noexcept : _get_obj(other._get_obj), _func(std::move(other._func)), argNames(std::move(other.argNames)) {} - // Copy assignment Wrapper& operator=(const Wrapper& other) { if (this != &other) { - _obj = std::make_unique(*other._obj); + _get_obj = other._get_obj; _func = other._func; argNames = other.argNames; } return *this; } - // Move assignment Wrapper& operator=(Wrapper&& other) noexcept { if (this != &other) { - _obj = std::move(other._obj); + _get_obj = std::move(other._get_obj); _func = other._func; argNames = std::move(other.argNames); } From aa373cff41ac1dcb45d1b99751c3f5d9a39d6cdf Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 13 Dec 2025 10:21:50 +0300 Subject: [PATCH 08/15] add wrapper tests --- tests/wrapper_test.cpp | 132 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 tests/wrapper_test.cpp diff --git a/tests/wrapper_test.cpp b/tests/wrapper_test.cpp new file mode 100644 index 0000000..605e51b --- /dev/null +++ b/tests/wrapper_test.cpp @@ -0,0 +1,132 @@ +#include +#include +#include +#include + +#include "Wrapper.hpp" + +class Subject { +public: + int double_it(int x) { return x * 2; } + double sum(double a, double b) { return a + b; } + void set_sum(int arg1, int arg2) { last_called = arg1 + arg2; } + std::string concat_num_to_string(const std::string& s, int n) { return s + std::to_string(n); } + + int last_called = 0; +}; + +TEST(WrapperTest, ConstructorValidArgs) { + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}}); + EXPECT_NO_THROW(wrapper.execute({{"x", 5}})); +} + +TEST(WrapperTest, ConstructorInvalidArgCountThrows) { + Subject subj; + const auto ctor = [&subj]() { + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}, {"y", 0}}); + }; + EXPECT_THROW( + ctor(), + std::invalid_argument + ); +} + +TEST(WrapperTest, ExecuteWithDefaultValues) { + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 10}}); + auto result = wrapper.execute({}); + EXPECT_EQ(std::any_cast(result), 20); // 10 * 2 +} + +TEST(WrapperTest, ExecuteWithProvidedValues) { + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}}); + auto result = wrapper.execute({{"x", 7}}); + EXPECT_EQ(std::any_cast(result), 14); // 7 * 2 +} + +TEST(WrapperTest, ExecuteWithWrongArgumentNameIgnored) { + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}}); + auto result = wrapper.execute({{"wrong_name", 99}}); + EXPECT_EQ(std::any_cast(result), 0); +} + +TEST(WrapperTest, ExecuteWithTypeMismatchThrows) { + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}}); + EXPECT_THROW( + wrapper.execute({{"x", std::string("not_an_int")}}), + std::invalid_argument + ); +} + +TEST(WrapperTest, ExecuteVoidFunction) { + Subject subj; + Wrapper wrapper(&subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 0}}); + EXPECT_NO_THROW(wrapper.execute({{"arg1", 3}, {"arg2", 4}})); + EXPECT_EQ(subj.last_called, 7); // 3+4 +} + +TEST(WrapperTest, ExecuteVoidFunctionReturnsEmptyAny) { + Subject subj; + Wrapper wrapper(subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 0}}); + auto result = wrapper.execute({{"arg1", 1}, {"arg2", 2}}); + EXPECT_FALSE(result.has_value()); //void = no value +} + +TEST(WrapperTest, CopyConstructor) { + Subject subj; + Wrapper wrapper1(subj, &Subject::double_it, {{"x", 0}}); + Wrapper wrapper2(wrapper1); + auto result = wrapper2.execute({{"x", 6}}); + EXPECT_EQ(std::any_cast(result), 12); +} + +TEST(WrapperTest, MoveConstructor) { + Subject subj; + Wrapper wrapper1(subj, &Subject::double_it, {{"x", 0}}); + Wrapper wrapper2(std::move(wrapper1)); + auto result = wrapper2.execute({{"x", 8}}); + EXPECT_EQ(std::any_cast(result), 16); +} + +TEST(WrapperTest, ExecuteWithTooManyArgsThrows) { + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}}); + EXPECT_THROW( + wrapper.execute({{"x", 5}, {"extra", 99}}), // extra arg + std::invalid_argument + ); +} + +TEST(WrapperTest, FunctionWithMultipleParameters) { + Subject subj; + Wrapper wrapper(subj, &Subject::sum, {{"a", 0.0}, {"b", 0.0}}); + auto result = wrapper.execute({{"a", 1.5}, {"b", 2.5}}); + EXPECT_DOUBLE_EQ(std::any_cast(result), 4.0); +} + +TEST(WrapperTest, FunctionWithStringParameter) { + Subject subj; + Wrapper wrapper(subj, &Subject::concat_num_to_string, {{"s", std::string("")}, {"n", 0}}); + auto result = wrapper.execute({{"s", std::string("Hello")}, {"n", 42}}); + EXPECT_EQ(std::any_cast(result), "Hello42"); +} + +TEST(WrapperTest, DefaultValueForString) { + Subject subj; + Wrapper wrapper(subj, &Subject::concat_num_to_string, {{"s", std::string("Default")}, {"n", 0}}); + auto result = wrapper.execute({{"n", 100}}); + EXPECT_EQ(std::any_cast(result), "Default100"); +} + +TEST(WrapperTest, ExecuteVoidFunctionWithNoReturn) { + Subject subj; + Wrapper wrapper(&subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); + auto result = wrapper.execute({{"arg1", 42}}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(subj.last_called, 43); +} + From f960e96a459283bdf5d03b7fc5ea0f74f19e0982 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 13 Dec 2025 10:48:23 +0300 Subject: [PATCH 09/15] fix ctor from unique ptr --- src/Wrapper.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Wrapper.hpp b/src/Wrapper.hpp index 9e04d4a..e5f9ab7 100644 --- a/src/Wrapper.hpp +++ b/src/Wrapper.hpp @@ -67,7 +67,7 @@ class Wrapper : public WrapperBase { } Wrapper(std::unique_ptr obj, Func func, const ArgList& argList): - _get_obj([obj = std::move(obj)]() -> T& { return *obj; }), + _get_obj([obj = std::shared_ptr(std::move(obj))]() -> T& { return *obj; }), _func(func), argNames(argList) { if (argList.size() != ARG_COUNT) From 03df1e4bb8f9dc6d046473e261d9e5056192739b Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 13 Dec 2025 10:48:36 +0300 Subject: [PATCH 10/15] add tests for ctors --- tests/wrapper_test.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/wrapper_test.cpp b/tests/wrapper_test.cpp index 605e51b..a870d49 100644 --- a/tests/wrapper_test.cpp +++ b/tests/wrapper_test.cpp @@ -130,3 +130,40 @@ TEST(WrapperTest, ExecuteVoidFunctionWithNoReturn) { EXPECT_EQ(subj.last_called, 43); } + +TEST(WrapperTest, ConstructWithConstRef) { + Subject subj; + Wrapper wrapper(subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); //copies subj internally + auto result = wrapper.execute({{"arg1", 42}}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(subj.last_called, 0); //original object is not changed +} + +TEST(WrapperTest, ConstructWithRawPtr) { + auto* subj = new Subject(); + Wrapper wrapper(subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); //just stores raw ptr + auto result = wrapper.execute({{"arg1", 42}}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(subj->last_called, 43); //original object is changed + + delete subj; //after deleting executing wrapper is UB, likely SEGFAULT +} + +TEST(WrapperTest, ConstructWithUniquePtr) { + auto subj = std::make_unique(); + Subject* observer = subj.get(); + + Wrapper wrapper(std::move(subj), &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); //unique ptrs have to be moved + + auto result = wrapper.execute({{"arg1", 42}}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(observer->last_called, 43); +} + +TEST(WrapperTest, ConstructWithSharedPtr) { + std::shared_ptr subj = std::make_shared(); + Wrapper wrapper(subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); //copies shared ptr inside. Can be moved tho + auto result = wrapper.execute({{"arg1", 42}}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(subj->last_called, 43); //original object is changed +} \ No newline at end of file From a75ec8cc9dec67cb87666193f09321ebfc951c03 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 13 Dec 2025 10:48:44 +0300 Subject: [PATCH 11/15] format --- src/Engine.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Engine.hpp b/src/Engine.hpp index 8da4eab..286bced 100644 --- a/src/Engine.hpp +++ b/src/Engine.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -20,7 +19,7 @@ class Engine { void register_command( const std::string& name, Obj obj, - Ret(T::*func)(Args...), + Ret (T::*func)(Args...), const WrapperBase::ArgList& argList ) { wrappers.emplace(name, std::make_unique>(obj, func, argList)); From 6c114a831e67bc71c1e2535d3c342e6f090b0d53 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 13 Dec 2025 10:48:57 +0300 Subject: [PATCH 12/15] add basic engine tests --- tests/engine_test.cpp | 73 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/engine_test.cpp diff --git a/tests/engine_test.cpp b/tests/engine_test.cpp new file mode 100644 index 0000000..20fe77b --- /dev/null +++ b/tests/engine_test.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include + +#include "Engine.hpp" +#include "Wrapper.hpp" + +class Subject { +public: + int double_it(int x) { return x * 2; } + double sum(double a, double b) { return a + b; } + void set_sum(int arg1, int arg2) { last_called = arg1 + arg2; } + std::string concat_num_to_string(const std::string& s, int n) { return s + std::to_string(n); } + + int last_called = 0; +}; + + +TEST(EngineTest, RegisterAndExecuteCommand) { + Engine engine; + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}}); + engine.register_command("double_it", wrapper); + + auto result = engine.execute("double_it", {{"x", 5}}); + EXPECT_EQ(std::any_cast(result), 10); +} + +TEST(EngineTest, ExecuteNonExistentCommandThrows) { + Engine engine; + EXPECT_THROW( + engine.execute("nonexistent", {}), + std::invalid_argument + ); +} + +TEST(EngineTest, ExecuteCommandWithWrongArgTypePropagatesError) { + Engine engine; + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}}); + engine.register_command("double_it", wrapper); + + EXPECT_THROW( + engine.execute("double_it", {{"x", std::string("hello")}}), + std::invalid_argument + ); +} + +TEST(EngineTest, RegisterWithCtorArgs) { + Engine engine; + Subject subj; + engine.register_command("double", &subj, &Subject::double_it, WrapperBase::ArgList{{"x", 0}}); + +} + +TEST(EngineTest, MultipleCommands) { + Engine engine; + Subject subj; + + engine.register_command("double", &subj, &Subject::double_it, WrapperBase::ArgList{{"x", 0}}); + engine.register_command("add", &subj, &Subject::sum, {{"a", 0.0}, {"b", 0.0}}); + engine.register_command("set_last", &subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 0}}); + + auto result1 = engine.execute("double", {{"x", 3}}); + EXPECT_EQ(std::any_cast(result1), 6); + + auto result2 = engine.execute("add", {{"a", 2.5}, {"b", 3.5}}); + EXPECT_DOUBLE_EQ(std::any_cast(result2), 6.0); + + engine.execute("set_last", {{"arg1", 10}, {"arg2", 20}}); + EXPECT_EQ(subj.last_called, 30); +} From 38d56b8d4581970a896039a6472d51e9dd420c15 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 13 Dec 2025 10:55:00 +0300 Subject: [PATCH 13/15] add register to engine with all types of object tests --- src/Engine.hpp | 2 +- tests/engine_test.cpp | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Engine.hpp b/src/Engine.hpp index 286bced..a22a6fb 100644 --- a/src/Engine.hpp +++ b/src/Engine.hpp @@ -22,7 +22,7 @@ class Engine { Ret (T::*func)(Args...), const WrapperBase::ArgList& argList ) { - wrappers.emplace(name, std::make_unique>(obj, func, argList)); + wrappers.emplace(name, std::make_unique>(std::move(obj), func, argList)); } diff --git a/tests/engine_test.cpp b/tests/engine_test.cpp index 20fe77b..17ee40e 100644 --- a/tests/engine_test.cpp +++ b/tests/engine_test.cpp @@ -51,7 +51,6 @@ TEST(EngineTest, RegisterWithCtorArgs) { Engine engine; Subject subj; engine.register_command("double", &subj, &Subject::double_it, WrapperBase::ArgList{{"x", 0}}); - } TEST(EngineTest, MultipleCommands) { @@ -71,3 +70,42 @@ TEST(EngineTest, MultipleCommands) { engine.execute("set_last", {{"arg1", 10}, {"arg2", 20}}); EXPECT_EQ(subj.last_called, 30); } + + +TEST(EngineTest, RegisterWithConstRef) { + Subject subj; + Engine engine; + engine.register_command("sum", subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); + auto result = engine.execute("sum", {{"arg1", 42}}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(subj.last_called, 0); //original object is not changed +} + +TEST(EngineTest, RegisterWithRawPtr) { + auto* subj = new Subject(); + Engine engine; + engine.register_command("sum", subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); + auto result = engine.execute("sum", {{"arg1", 42}}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(subj->last_called, 43); //original object is changed +} + +TEST(EngineTest, RegisterWithUniquePtr) { + auto subj = std::make_unique(); + Subject* observer = subj.get(); + + Engine engine; + engine.register_command("sum", std::move(subj), &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); + auto result = engine.execute("sum", {{"arg1", 42}}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(observer->last_called, 43); //original object is changed +} + +TEST(EngineTest, RegisterWithSharedPtr) { + std::shared_ptr subj = std::make_shared(); + Engine engine; + engine.register_command("sum", subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); + auto result = engine.execute("sum", {{"arg1", 42}}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(subj->last_called, 43); //original object is changed +} From 052214e6d3f9b540235e7775099509b9f56280f6 Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 13 Dec 2025 11:46:30 +0300 Subject: [PATCH 14/15] add docs --- README.md | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..aeecbc0 --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +# Engine-Wrapper лабораторная работа + +## Table of Contents //TODO + +- [Что такое ArgList](#ArgList) +- [Wrapper](#Wrapper) + - [Описание конструкторов](#все-конструкторы-класса-wrapper-принимают) + - [`execute()`](#execute) + - [Примеры](#examples) +- [Engine](#engine) + - [Что он может](#что-он-может) + - [Примеры](#примеры) +- [Сборка и запуск](#сборка-и-запуск) + +--- + +## `ArgList` + +### `using ArgList = std::vector>` + +### Возможный синтаксис: + +1. Пустой: `{}` +2. 1 аргумент: `{{"a", 5}}` +3. n аргументов: `{{"a", 6}, {...}, ..., {"z", 7}}` +4. Можно просто использовать свой вектор + +--- + +## `Wrapper` + +### Все конструкторы класса `Wrapper` принимают: + +1. Объект, указатель на объект, `unique_ptr` или `shared_ptr` на объект +2. Ссылку на функцию (должна быть функцией объекта, статичные, константные, `volatile` функции не поддерживаются) +3. `ArgList` - `{}` (empty) or `{{"a", 5}}` (первый аргумент функции теперь называется `a` и имеет дефолтное значение + `5`). Количество аргументов в ArgList должно точно совпадать с количеством аргументов функции, так как на этом этапе + мы даем им внутреннее имя, которое в дальнейшем будет использоваться при вызове функции + +### `execute()` + +- Принимает `ArgList`. Его размер должен быть не больше количества аргументов функции, а типы аргументов должны + совпадать (Если `a` раньше было `int`, то сейчас тоже должно быть `int`. Кастинг не поддерживается) +- Сопоставляет аргументы по именам. Если в функции не передали какой-то аргумент, используется его дефолтное (указанное + при конструировании) значение + +### Examples: + +#### Какой-то класс `Subject`: + +```c++ +class Subject { +public: + int double_it(int x) { return x * 2; } + double sum(double a, double b) { return a + b; } + void set_sum(int arg1, int arg2) { last_called = arg1 + arg2; } + std::string concat_num_to_string(const std::string& s, int n) { return s + std::to_string(n); } + + int last_called = 0; +}; +``` + +#### Базовые use-case: + +1. `execute()` без аргументов: + +```c++ + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 10}}); + auto result = wrapper.execute({}); + std::any_cast(result); //== 10 * 2 +``` + +2. `execute()` с аргументами: + +```c++ + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}}); + auto result = wrapper.execute({{"x", 7}}); + std::any_cast(result); // == 7 * 2 +``` + +#### Все варианты использования конструктора: + +1. Копирование объекта внутрь (по `const ref`) + +```c++ + Subject subj; + Wrapper wrapper(subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); //copies subj internally + auto result = wrapper.execute({{"arg1", 42}}); + result.has_value(); //is false due to function returns void + subj.last_called; // == 0 as original object is not changed +``` + +2. Raw ptr. Будьте осторожны, очень велика вероятность нарваться на segfault, если `execute()` будет после того, как объект умрет. Используйте на свой страх и риск! Smart pointerы предпочтительнее. + +```c++ + auto* subj = new Subject(); + Wrapper wrapper(subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); //just stores raw ptr + auto result = wrapper.execute({{"arg1", 42}}); + result.has_value(); //still false + subj->last_called; // == 43, original object is changed + delete subj; //after deleting executing wrapper is UB, likely SEGFAULT, as raw pointer is just stored inside Wrapper +``` + +3. Unique ptr + +```c++ + auto subj = std::make_unique(); + Subject* observer = subj.get(); + + Wrapper wrapper(std::move(subj), &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); //unique ptrs have to be moved + //subj is now nullptr + auto result = wrapper.execute({{"arg1", 42}}); + result.has_value(); //still false + observer->last_called; // == 43, original object is changed +``` + +4. Shared ptr + +```c++ + std::shared_ptr subj = std::make_shared(); + Wrapper wrapper(subj, &Subject::set_sum, {{"arg1", 0}, {"arg2", 1}}); //copies shared ptr inside. Can be moved tho + auto result = wrapper.execute({{"arg1", 42}}); + result.has_value(); //still false + subj->last_called; // == 43, original object is changed +``` + +--- + +## Engine +Регистр зарегистрированных команд, которые можно запускать + +### Что он может: +1. Зарегистрировать команду: `Engine.register_command("name", wrapper)` +2. In-place создать `Wrapper` и зарегистрировать команду: `Engine.register_command("name", subj, Subject::sum, {{"a", 0}, {"b", 0}})` - поддерживает все конструкторы Wrapper'a (`subj` может быть raw ptr, const ref, unique ptr or shared ptr) +3. Запускать команды: `Engine.execute("name", {/*args here*/})` + + +Если команда не была зарегистрирована, выкинет `std::invalid_argument` +Все ошибки, которые возникнут внутри `Wrapper`, в нем не обрабатываются и передаются дальше + +### Примеры: +```c++ + Engine engine; + Subject subj; + Wrapper wrapper(subj, &Subject::double_it, {{"x", 0}}); + engine.register_command("double_it", wrapper); + + auto result = engine.execute("double_it", {{"x", 5}}); + std::any_cast(result); // 5*2 +``` + +--- + +## Tests +Еще больше примеров и исходники тестов можно найти в папке [тестов](tests) + +## Сборка и запуск +### Сконфигурируйте CMake: +`cmake -S ./ -B ./build` (`-G Ninja` по желанию) + +### Сборка и запуск: +1. Сборка: `cmake --build ./build` +2. Запуск основной программы ([main.cpp](main.cpp)): `./build/EngineWrapper` - должно вывести 16 +3. Запуск тестов: `./build/tests/EngineWrapperTests` - тут GTests From 6f062fe3a38cab7c62f3794b2ad38677b9de44cb Mon Sep 17 00:00:00 2001 From: Fromant Date: Sat, 13 Dec 2025 11:46:38 +0300 Subject: [PATCH 15/15] format --- main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index 02e8008..8e17952 100644 --- a/main.cpp +++ b/main.cpp @@ -9,7 +9,7 @@ struct A { int main() { A obj; - Wrapper w1(obj, &A::f, {{"a",0},{"b",0}}); // non-const member + Wrapper w1(obj, &A::f, {{"a",0},{"b",0}}); Engine e; e.register_command("w1", w1);