草庐IT

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

coder 2024-02-24 原文

我一直在尝试完全理解 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++ - 如何将这种重用存储撤消堆栈模式从通用 vector 调整为类型化模型?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  8. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  9. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  10. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

随机推荐