Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
166 changes: 166 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<std::pair<std::string, std::any>>`

### Возможный синтаксис:

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<int>(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<int>(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>();
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<Subject> subj = std::make_shared<Subject>();
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<int>(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
19 changes: 19 additions & 0 deletions main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <iostream>

#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<int>(e.execute("w1", {{"b", 16}})) << std::endl;
}
40 changes: 40 additions & 0 deletions src/Engine.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include <any>
#include <memory>
#include <stdexcept>
#include <string>
#include <unordered_map>

#include "Wrapper.hpp"

class Engine {
public:
template <typename T, typename Ret, typename... Args>
void register_command(const std::string& name, const Wrapper<T, Ret, Args...>& wrapper) {
wrappers.emplace(name, std::make_unique<Wrapper<T, Ret, Args...>>(wrapper));
}

template <typename Obj, typename T, typename Ret, typename... Args>
void register_command(
const std::string& name,
Obj obj,
Ret (T::*func)(Args...),
const WrapperBase::ArgList& argList
) {
wrappers.emplace(name, std::make_unique<Wrapper<T, Ret, Args...>>(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<std::string, std::unique_ptr<WrapperBase>> wrappers;
};
127 changes: 127 additions & 0 deletions src/Wrapper.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#pragma once

#include <any>
#include <functional>
#include <stdexcept>
#include <string>
#include <typeindex>
#include <unordered_map>

#include "WrapperBase.hpp"


// T is a object type
// Ret is a function's return type
// Args is a function's parameters types
template <typename T, typename Ret, typename... Args>
class Wrapper : public WrapperBase {
static constexpr size_t ARG_COUNT = sizeof...(Args);

using ArgMap = std::unordered_map<std::string, std::any>;
using Func = Ret (T::*)(Args...);

Func _func;

std::function<T&()> _get_obj;

ArgList argNames;
std::array<std::type_index, ARG_COUNT> argTypes = {typeid(Args)...};

template <typename R = Ret, std::size_t... Indices>
std::enable_if_t<!std::is_void_v<R>, std::any>
invoke_function(const std::array<std::any, ARG_COUNT>& args, std::index_sequence<Indices...>) const {
return (_get_obj().*_func)(std::any_cast<Args>(args[Indices])...);
}

// Helper for void return
template <typename R = Ret, std::size_t... Indices>
std::enable_if_t<std::is_void_v<R>, std::any>
invoke_function(const std::array<std::any, ARG_COUNT>& args, std::index_sequence<Indices...>) const {
(_get_obj().*_func)(std::any_cast<Args>(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<T> 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<T> obj, Func func, const ArgList& argList):
_get_obj([obj = std::shared_ptr<T>(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<std::any, ARG_COUNT> 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<std::string, std::any>& 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<ARG_COUNT>{});
}
};
12 changes: 12 additions & 0 deletions src/WrapperBase.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once
#include <any>
#include <vector>
#include <string>

class WrapperBase {
public:
using ArgList = std::vector<std::pair<std::string, std::any>>;

virtual std::any execute(const ArgList& args) const = 0;
virtual ~WrapperBase() = default;
};
Loading