c++ - 如何将这种重用存储撤消堆栈模式从通用 vector 调整为类型化模型?

标签 c++ undo

我一直在尝试完全理解 Sean Parent's talk "Inheritance Is The Base Class of Evil" 中演示的撤消模式.演讲涵盖了很多基础知识,包括 C++ 移动语义,以及使用概念来实现多态性而不是继承,但增量撤消存储模式是我一直试图了解的模式。这是 Parent 在他的演讲中给出的示例的工作改编:

#include <iostream>
#include <memory>
#include <vector>
#include <assert.h>

using namespace std;

template <typename T>
void draw(const T& x, ostream& out, size_t position)
{
    out << string(position, ' ') << x << endl;
}


class object_t {
public:
    template <typename T>
    object_t(T x) : self_(make_shared<model<T>>(move(x))) {}
    friend void draw(const object_t& x, ostream& out, size_t position)
    { x.self_->draw_(out, position); }
private:
    struct concept_t {
        virtual ~concept_t() = default;
        virtual void draw_(ostream&, size_t) const = 0;
    };
    template <typename T>
    struct model : concept_t {
        model(T x) : data_(move(x)) { }
        void draw_(ostream& out, size_t position) const
        { draw(data_, out, position); }
        T data_; };
    shared_ptr<const concept_t> self_;
};


// The document itself is drawable
using document_t = vector<object_t>;
void draw(const document_t& x, ostream& out, size_t position)
{
    out << string(position, ' ') << "<document>" << endl;
    for (const auto& e : x) draw(e, out, position + 2);
    out << string(position, ' ') << "</document>" << endl;
}

// An arbitrary class
class my_class_t {
    /* ... */
};

void draw(const my_class_t&, ostream& out, size_t position)
{ out << string(position, ' ') << "my_class_t" << endl; }

// Undo management...
using history_t = vector<document_t>;
void commit(history_t& x) { assert(x.size()); x.push_back(x.back()); }
void undo(history_t& x) { assert(x.size()); x.pop_back(); }
document_t& current(history_t& x) { assert(x.size()); return x.back(); }

// Usage example.
int main(int argc, const char * argv[])
{

    history_t h(1);


    current(h).emplace_back(0);
    current(h).emplace_back(string("Hello!"));
    draw(current(h), cout, 0);
    cout << "--------------------------" << endl;

    commit(h);

    current(h).emplace_back(current(h));
    current(h).emplace_back(my_class_t());
    current(h)[1] = string("World");
    draw(current(h), cout, 0);

    cout << "--------------------------" << endl;
    undo(h);
    draw(current(h), cout, 0);

    return EXIT_SUCCESS;
}

此模式不是将撤消跟踪为捕获其前后状态的命令堆栈,而是将撤消状态跟踪为“整个文档”的堆栈,其中每个条目实际上都是文档的完整拷贝。该模式的诀窍在于,使用一些间接和 shared_ptr,仅针对文档中每个状态之间不同的部分进行存储/分配。每个“拷贝”只会因它与先前状态之间的差异而招致存储惩罚。

Parent 示例中的模式表明“当前”文档是完全可变的,但是当您对历史调用 commit 时,它会提交到历史中。这会将当前状态的“拷贝”推送到历史记录中。

总的来说,我觉得这个模式很有说服力。 Parent 在本次演讲中展示的示例显然主要是为了展示他关于基于概念的多态性和移动语义的观点。相对于那些,撤消模式感觉是辅助的,尽管我认为它的作用是指出值语义的值(value)。

在示例中,文档“模型”只是“符合概念的对象 vector ”。这符合演示的目的,但我发现很难从“概念 vector ”推断为“真实世界的类型化模型”。 (就此问题而言,基于概念的多态性是不相关的。)因此,例如,考虑以下简单模型,其中“文档”是一个 company,其中包含一些员工,每个员工都有姓名、薪水和照片:

struct image {
    uint8_t bitmapData[640 * 480 * 4];
};

struct employee {
    string name;
    double salary;
    image picture;
};

struct company {
    string name;
    string bio;
    vector<employee> employees;
};

我的问题是:如何在不失去直接和简单地与模型交互的能力的情况下引入获得存储共享所必需的间接?通过交互的简单性,我的意思是您应该能够继续以直接的方式与模型交互,而无需大量的 RTTI 或转换等。例如,如果您试图给名为“Susan”的每个人加薪 10% ,在每次更改后捕获撤消状态,一个简单的交互可能看起来像这样:

using document_t = company;
using history_t = vector<document_t>;
void commit(history_t& x) { assert(x.size()); x.push_back(x.back()); }
void undo(history_t& x) { assert(x.size()); x.pop_back(); }
document_t& current(history_t& x) { assert(x.size()); return x.back(); }

void doStuff()
{
    history_t h(1);
    for (auto& e : current(h).employees)
    {
        if (e.name.find("Susan") == 0)
        {
            e.salary *= 1.1;
            commit(h);
        }
    }
}

技巧似乎是注入(inject) object_t 提供的间接寻址,但不清楚我如何既可以引入必要的间接寻址又可以 随后透明地遍历该间接寻址。我通常可以使用 C++ 代码,但这不是我的日常语言,所以这很可能是非常简单的事情。无论如何,Parent 的示例没有涵盖这一点也就不足为奇了,因为他的大部分观点是通过使用概念隐藏类型的能力。

有人对此有任何想法吗?

最佳答案

虽然文档是可变的,但对象不是。

要编辑对象,您需要创建一个新对象。

在实际的解决方案中,每个对象都可能是一个可写入的智能指针持有者,您可以通过读取或写入访问它。写访问会复制对象,前提是它的引用计数大于 1。

如果您愿意将所有变更限制在访问器方法中,您可以在其中进行写时复制。如果不是,get_writable 方法会在写入时进行复制。请注意,修改通常也意味着修改一直返回到根目录,因此您的写入方法可能需要获取到根目录的路径,写入时的拷贝将传播到那里。或者,您可以使用文档上下文和 guid 等效标识符和 HashMap ,因此编辑 bar 中包含的 foo 会保持 bar 不变,因为它通过名称而不是指针来标识 foo。

关于c++ - 如何将这种重用存储撤消堆栈模式从通用 vector 调整为类型化模型?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20577521/

相关文章:

c++ - 如何在 C++ 中显示长度超过 1 个小数点的答案

c++ - 无法在类中前向声明类

python - 如何使用 Python/Django 实现 "undo"功能

c++ - 链接GoogleTest和CodeBlocks

c++ - 使用 libpcap 中断捕获的问题

c# - C# 中的命令模式和复杂操作

haskell - 使用堆栈实现撤消和重做功能。如何编辑堆栈而无需在 Haskell 中重新创建它

Qt 清除 QTextEdit/QPlainTextEdit 中的撤消历史记录吗?

c++ - 修复了带引用的内存布局同步变量

MATLAB 撤消命令