Каким способом можно хранить действия?

Допустим, у нас есть:

  • какой-то объект
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 шт):

Автор решения: bloody

В вашей задаче я бы поступил так: Для каждой функции бы создал в глобальной области видимости структуру которая содержит изменения которая внесла функция. При этом не копируя весь вектор, а лишь те элементы, которые изменились. Чтобы экономить память и не замедлять работу нужно добавить идентификатор который будет говорить о том, что это изменение уже есть (Или же просто проверять есть ли такое изменение. Но мне было лень это делать)

Примерная реализация:

#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;
}
→ Ссылка
Автор решения: LShadow77

Я так понимаю, то что вы желаете получить, это механизм 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, из кода можно убрать.

→ Ссылка