Каким способом можно хранить действия?
Допустим, у нас есть:
- какой-то объект
struct foo {
int a;
int b;
int c;
};
- массив таких объектов
vector<foo> objects = {foo{1, 2, 3}, ..., foo{432, 33, 189}};
- и много функций, которые совершают какие-то преобразования
void transformation_1(size_t n, vector<foo>& objects) {
// У соседних элементов (n - 1, n + 1) усредняет a, b и c.
int new_a = (objects[n - 1].a + objects[n + 1].a) / 2;
int new_b = (objects[n - 1].b + objects[n + 1].b) / 2;
int new_c = (objects[n - 1].c + objects[n + 1].c) / 2;
objects[n - 1].a = new_a;
objects[n - 1].b = new_b;
objects[n - 1].c = new_c;
objects[n + 1].a = new_a;
objects[n + 1].b = new_b;
objects[n + 1].c = new_c;
}
void transformation_2(size_t n, vector<foo>& objects) {
// Добавляет 4 элемента.
int a = objects[n].a;
int b = objects[n].b;
int c = objects[n].c;
objects.emplace_back(foo{a / 4, b / 4, c / 4});
objects.emplace_back(foo{a / 2, b / 2, c / 2});
objects.emplace_back(foo{a * 2, b * 2, c * 2});
objects.emplace_back(foo{a * 4, b * 4, c * 4});
}
// ...
void transformation_100(size_t n, vector<foo>& objects) {
// Удаляет элемент n.
objects.erase(remove(objects.begin(), objects.end(), n), objects.end());
}
Есть функции, которые выполняют определенное количество преобразований
void func_1(vector<foo>& objects) {
transformation_1(1, objects);
transformation_27(999, objects);
// ...
transformation_8(9'999'999, objects);
}
void func_2(vector<foo>& objects) {
transformation_56(1, objects);
transformation_43(999, objects);
// ...
transformation_11(9'999'999, objects);
}
// ...
void func_100(vector<foo>& objects) {}
В основном алгоритме по некой логике выбирается функция из предыдущего списка и самое главное, что, если преобразование окажется плохим, то нужно откатить изменения назад.
В простейшем случае достаточно было бы хранить копию массива объектов и в случае, когда преобразование было плохим, присваивать массиву предыдущее состояние:
int main(){
vector<foo> objects = {foo{1, 2, 3}, ..., foo{432, 33, 189}};
for(;;){
// Тут происходит копирование
vector<foo> objects_prev = objects;
// Выбирается метод преобразования из func_1 ... func_100
func_[1, ..., 100](objects);
// Если плохое преобразование
if(is_bad(objects)){
// Вернуть предыдущее состояние
objects = move(objects_prev);
}
}
}
Но такой метод очень медленный, потому что на каждой итерации приходится копировать массив объектов, которых может быть и 2Гб. Поэтому я хочу реализовать логику отмены изменений, но я нигде не нашел информации о таком и не знаю, как лучше это сделать.
Я предполагаю, что каждая функция transformation должна записывать информацию о проделанном действии и на каждое действие должно быть реализовано анти-действие. И когда приходит время отменить изменения, нужно по порядку с конца вызывать анти-действие для каждого действия.
Это было бы хорошим решением или есть что-то более умное?
p.s. Буду благодарен, если поделитесь каким-нибудь источником, где есть описание или реализация чего-то похожего.
Ответы (2 шт):
В вашей задаче я бы поступил так: Для каждой функции бы создал в глобальной области видимости структуру которая содержит изменения которая внесла функция. При этом не копируя весь вектор, а лишь те элементы, которые изменились. Чтобы экономить память и не замедлять работу нужно добавить идентификатор который будет говорить о том, что это изменение уже есть (Или же просто проверять есть ли такое изменение. Но мне было лень это делать)
Примерная реализация:
#include <iostream>
#include <vector>
using namespace std;
struct NeedData
{
size_t a = 0, b = 0, c = 0;
};
struct _ELEM_CHANGE
{
_ELEM_CHANGE(NeedData _NEEDDATA, size_t _ID_CHANGE, size_t _INDEX)
{
_DATA = _NEEDDATA;
_idChange__ = _ID_CHANGE;
_Index_CHANGE = _INDEX;
}
size_t _idChange__ = 0;
NeedData _DATA = { 0,0,0 };
size_t _Index_CHANGE = 0;
};
struct _LAST_CHANGE_
{
vector<_ELEM_CHANGE>_LAST_DAT;
};
_LAST_CHANGE_ RANDOMFUNC_CHANGE;
#define LASTCHANGE_ID_RANDOMFUNC 8238429
bool id_Is_real(size_t ID, _LAST_CHANGE_ CHANGE)
{
for (size_t x = 0; x < CHANGE._LAST_DAT.size(); x++)
{
if (CHANGE._LAST_DAT[x]._idChange__ == ID)
{
return true;
}
}
return false;
}
void randomFunc(vector<NeedData>& Dat, size_t _Index)
{
if (Dat.size() < _Index + 1)
{
return;
}
if (!id_Is_real(LASTCHANGE_ID_RANDOMFUNC, RANDOMFUNC_CHANGE))
{
RANDOMFUNC_CHANGE._LAST_DAT.push_back({ Dat[_Index], LASTCHANGE_ID_RANDOMFUNC, _Index });
}
Dat[_Index].a = 12;
Dat[_Index].b = 13;
Dat[_Index].c = 14;
}
void CancelLast(vector<NeedData>& Dat, _LAST_CHANGE_ Changes, size_t id_changes)
{
for (size_t x = 0; x < Changes._LAST_DAT.size(); x++)
{
if (Changes._LAST_DAT[x]._idChange__ == id_changes)
{
Dat[Changes._LAST_DAT[x]._Index_CHANGE] = Changes._LAST_DAT[x]._DATA;
}
}
}
int main()
{
vector<NeedData> RandomVector;
RandomVector.push_back({ 16, 16, 16 });
cout << "Vector at start: " << endl;
cout << RandomVector[0].a << endl;
cout << RandomVector[0].b << endl;
cout << RandomVector[0].c << endl;
randomFunc(RandomVector, 0);
cout << "RandomFunc: " << endl;
cout << RandomVector[0].a << endl;
cout << RandomVector[0].b << endl;
cout << RandomVector[0].c << endl;
CancelLast(RandomVector, RANDOMFUNC_CHANGE, LASTCHANGE_ID_RANDOMFUNC);
cout << "Cancel: " << endl;
cout << RandomVector[0].a << endl;
cout << RandomVector[0].b << endl;
cout << RandomVector[0].c << endl;
system("PAUSE");
return 1;
}
Я так понимаю, то что вы желаете получить, это механизм Undo/Redo, аналогичный тому, который реализован в редакторах. В таком случае действия целесообразно представить не в виде функций, а как классы. Эти классы должны содержать минимальный набор данных, необходимых для отката (undo) и возврата (redo) того или иного действия, пару соответствующих виртуальных методов Undo(), Redo(). А роль самой функции действия (transformation) может взять на себя, например, конструктор.
Абстрактный класс для действия в первом приближении может выглядеть так:
//абстрактный класс действия
class CAction
{
protected:
bool m_bSuccess; //true, если действие выполнено успешно
const char* m_pstrName; //имя действия (если необходимо их как-то логить)
public:
CAction(): m_bSuccess(false), m_pstrName("Unknown Action") {}
//геттеры
bool IsSuccess() {return m_bSuccess;}
const char* GetName() {return m_pstrName;}
virtual bool Undo() = 0;
virtual bool Redo() = 0;
//virtual ~CAction(); //деструктор тоже можно сделать виртуальным, если это необходимо
};
А унаследованные от него действия, на примере transformation_1 и transformation_2, будут выглядеть так:
/* ------------------------------ */
class CAction_Transform1: public CAction
{
//информация Undo/Redo для данного действия
std::vector<foo>& m_objects; //ссылка на вектор, над которым выполняется действие
size_t m_index; //сохранённый индекс (n)
foo m_oldLeft; //исходное objects[n - 1]
foo m_oldRight; //исходное objects[n + 1]
foo m_newVal; //новое значение для objects[n - 1] и objects[n + 1]
public:
CAction_Transform1(size_t n, std::vector<foo>& objects);
virtual bool Undo();
virtual bool Redo();
};
//собственно конструктор-действие
CAction_Transform1::CAction_Transform1(size_t n, std::vector<foo>& objects): m_objects(objects)
{
if ((n < 1) || (n >= objects.size()-1)) return;
m_pstrName = "transformation_1";
m_index = n;
// У соседних элементов (n - 1, n + 1) усредняет a, b и c.
foo newVal;
newVal.a = (objects[n - 1].a + objects[n + 1].a) / 2;
newVal.b = (objects[n - 1].b + objects[n + 1].b) / 2;
newVal.c = (objects[n - 1].c + objects[n + 1].c) / 2;
m_oldLeft = objects[n - 1];
m_oldRight = objects[n + 1];
m_newVal = newVal;
objects[n - 1] = newVal;
objects[n + 1] = newVal;
m_bSuccess = true; //не забываем установить "успех"
}
//делаем Undo
bool CAction_Transform1::Undo()
{
assert (m_bSuccess);
int n = m_index;
objects[n - 1] = m_oldLeft;
objects[n + 1] = m_oldRight;
return true;
}
//делаем Redo
bool CAction_Transform1::Redo()
{
assert (m_bSuccess);
int n = m_index;
objects[n - 1] = m_newVal;
objects[n + 1] = m_newVal;
return true;
}
/* ------------------------------ */
class CAction_Transform2: public CAction
{
//информация Undo/Redo для данного действия
std::vector<foo>& m_objects; //ссылка на вектор, над которым выполняется действие
foo m_newStructs[4];
public:
CAction_Transform2(size_t n, std::vector<foo>& objects);
virtual bool Undo();
virtual bool Redo();
};
CAction_Transform2::CAction_Transform2(size_t n, std::vector<foo>& objects): m_objects(objects)
{
if (n >= objects.size()) return;
m_pstrName = "transformation_2";
// Добавляет 4 элемента.
int a = objects[n].a;
int b = objects[n].b;
int c = objects[n].c;
m_newStructs[0] = foo{a / 4, b / 4, c / 4};
m_newStructs[1] = foo{a / 2, b / 2, c / 2};
m_newStructs[2] = foo{a * 2, b * 2, c * 2};
m_newStructs[3] = foo{a * 4, b * 4, c * 4};
for (char i=0; i<4; ++i) objects.emplace_back(m_newStructs[i]);
m_bSuccess = true;
}
//делаем Undo
bool CAction_Transform2::Undo()
{
assert (m_bSuccess);
for (char i=0; i<4; ++i) m_objects.pop_back();
return true;
}
//делаем Redo
bool CAction_Transform2::Redo()
{
assert (m_bSuccess);
for (char i=0; i<4; ++i) m_objects.emplace_back(m_newStructs[i]);
return true;
}
Также необходим класс-менеджер действий, представляющий по сути два стека (один для Undo, второй для Redo). Например, на скорую руку я набросал этот класс таким образом:
//класс менеджера действий
class CActionManager
{
std::stack<CAction*> m_UndoStack; //стек выполненных действий
std::stack<CAction*> m_RedoStack; //стек отменённых действий
public:
CActionManager() {}
CActionManager(CActionManager&) = delete;
~CActionManager();
bool AddAction(CAction* pAct); //добавляет новое действие в стек Undo
bool Undo(); //откат последнего действия
bool Redo(); //возврат последнего действия
void ClearRedo(); //очищает стек Redo
void Clear(); //очищает оба стека
};
CActionManager::~CActionManager()
{
assert (m_UndoStack.empty());
assert (m_RedoStack.empty());
}
//добавляет новое действие в стек Undo
bool CActionManager::AddAction(CAction* pAct)
{
assert (pAct!=nullptr);
ClearRedo(); //очищаем стек Redo
if (pAct->IsSuccess())
{
m_UndoStack.push(pAct); //кладём новое действие в стек Undo
return true;
}
delete pAct; //удаляем неудачное действие
return false;
}
//откат последнего действия
bool CActionManager::Undo()
{
if (!m_UndoStack.empty())
{
//выполняем откат
CAction* pAct = m_UndoStack.top();
if (!pAct->Undo()) return false;
//перебрасываем в стек Redo
m_UndoStack.pop();
m_RedoStack.push(pAct);
return true;
}
return false;
}
//возврат последнего действия
bool CActionManager::Redo()
{
if (!m_RedoStack.empty())
{
//выполняем возврат
CAction* pAct = m_RedoStack.top();
if (!pAct->Redo()) return false;
//перебрасываем в стек Undo
m_RedoStack.pop();
m_UndoStack.push(pAct);
return true;
}
return false;
}
//очищает стек Redo
void CActionManager::ClearRedo()
{
while (!m_RedoStack.empty())
{
CAction* pAct = m_RedoStack.top();
m_RedoStack.pop();
delete pAct;
}
}
//очищает оба стека
void CActionManager::Clear()
{
ClearRedo();
while (!m_UndoStack.empty())
{
CAction* pAct = m_UndoStack.top();
m_UndoStack.pop();
delete pAct;
}
}
Соответственно, проиллюстрирую использование всего выше написанного (код исключительно демонстрационный):
int main()
{
CActionManager actMgr;
char act;
size_t n;
setlocale(LC_ALL, "Russian");
printf(
"Действия:\n"
" 1 - transform_1\n"
" 2 - transform_2\n"
" u - undo\n"
" r - redo\n"
" q - выход\n"
);
while (true)
{
show_state();
printf("Введите действие: ");
do
{
act = getc(stdin);
} while (act=='\n');
if (act=='q') break;
switch (act)
{
case '1':
printf("Ведите n: ");
scanf("%u", &n);
if (!actMgr.AddAction(new CAction_Transform1(n, objects)))
printf("ERROR: can't do the action!\n");
continue;
case '2':
printf("Ведите n: ");
scanf("%u", &n);
if (!actMgr.AddAction(new CAction_Transform2(n, objects)))
printf("ERROR: can't do the action!\n");
continue;
case 'u':
if (!actMgr.Undo())
printf("ERROR: can't undo!\n");
continue;
case 'r':
if (!actMgr.Redo())
printf("ERROR: can't redo!\n");
continue;
}
}
actMgr.Clear();
return 0;
}
void show_state()
{
printf("\nObjects: \n");
for (auto p=objects.begin(); p!=objects.end(); ++p)
printf(" {%d, %d, %d}\n", p->a, p->b, p->c);
printf("\n");
}
Разумеется выше приведённый мною код не лишён недостатков. Например, для выполнения каждого действия используется динамическое выделение памяти, что может отрицательно сказываться на производительности и приводить к фрагментации памяти (если число выполняемых/откатываемых действий в единицу времени окажется велико) - это можно решить, используя принцип аккумулятора (предсозданного пула объектов), либо реализовав блочный аллокатор (с перегрузкой оператора new()).
Подведу итог: я не знаю конкретно вашего алгоритма и вашей задачи, однако описанный подход (паттерн, если угодно) довольно универсален, и надеюсь, окажется для вас полезен. Если возврат отменённых действий не предусмотрен, то всё, что связано с Redo, из кода можно убрать.