diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6b3cd48 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.14) +project(EngineWrapper) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(EngineWrapper main.cpp) +target_include_directories(EngineWrapper PRIVATE src) + +add_subdirectory(tests) 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 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..8e17952 --- /dev/null +++ b/main.cpp @@ -0,0 +1,19 @@ +#include + +#include "Engine.hpp" +#include "src/Wrapper.hpp" + +struct A { + int f(int a, int b) { return a + b; } +}; + +int main() { + A obj; + Wrapper w1(obj, &A::f, {{"a",0},{"b",0}}); + + Engine e; + e.register_command("w1", w1); + e.register_command("w2", std::move(w1)); + + std::cout << std::any_cast(e.execute("w1", {{"b", 16}})) << std::endl; +} diff --git a/src/Engine.hpp b/src/Engine.hpp new file mode 100644 index 0000000..a22a6fb --- /dev/null +++ b/src/Engine.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Wrapper.hpp" + +class Engine { +public: + template + void register_command(const std::string& name, const Wrapper& wrapper) { + wrappers.emplace(name, std::make_unique>(wrapper)); + } + + template + void register_command( + const std::string& name, + Obj obj, + Ret (T::*func)(Args...), + const WrapperBase::ArgList& argList + ) { + wrappers.emplace(name, std::make_unique>(std::move(obj), func, argList)); + } + + + 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->execute(args); + } + +private: + std::unordered_map> wrappers; +}; diff --git a/src/Wrapper.hpp b/src/Wrapper.hpp new file mode 100644 index 0000000..e5f9ab7 --- /dev/null +++ b/src/Wrapper.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#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 : public WrapperBase { + static constexpr size_t ARG_COUNT = sizeof...(Args); + + using ArgMap = std::unordered_map; + using Func = Ret (T::*)(Args...); + + 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 (_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 { + (_get_obj().*_func)(std::any_cast(args[Indices])...); + return {}; + } + +public: + 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::shared_ptr(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): _get_obj(other._get_obj), _func(other._func), + argNames(other.argNames) {} + + Wrapper(Wrapper&& other) noexcept : _get_obj(other._get_obj), _func(std::move(other._func)), + argNames(std::move(other.argNames)) {} + + Wrapper& operator=(const Wrapper& other) { + if (this != &other) { + _get_obj = other._get_obj; + _func = other._func; + argNames = other.argNames; + } + return *this; + } + + Wrapper& operator=(Wrapper&& other) noexcept { + if (this != &other) { + _get_obj = std::move(other._get_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"); + } + 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 { + if (it->second.type() != argTypes[i]) { + throw std::invalid_argument("Type mismatch for argument: " + name); + } + args[i] = it->second; + } + } + + return invoke_function(args, std::make_index_sequence{}); + } +}; diff --git a/src/WrapperBase.hpp b/src/WrapperBase.hpp new file mode 100644 index 0000000..2ae1755 --- /dev/null +++ b/src/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/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) + diff --git a/tests/engine_test.cpp b/tests/engine_test.cpp new file mode 100644 index 0000000..17ee40e --- /dev/null +++ b/tests/engine_test.cpp @@ -0,0 +1,111 @@ +#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); +} + + +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 +} diff --git a/tests/wrapper_test.cpp b/tests/wrapper_test.cpp new file mode 100644 index 0000000..a870d49 --- /dev/null +++ b/tests/wrapper_test.cpp @@ -0,0 +1,169 @@ +#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); +} + + +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