C++ 保存/加载+的机制;用最少的样板文件撤消/重做

C++ 保存/加载+的机制;用最少的样板文件撤消/重做,c++,C++,我想制作一个应用程序,用户可以编辑一个图表(例如),它将提供标准的机制:保存、加载、撤消和重做 一个简单的方法是为图表和其中的各种形状创建类,这些类通过save和load方法实现序列化,所有编辑它们的方法都返回UndoableActions,可以将其添加到UndoManager中,后者调用它们的perform方法并将它们添加到撤销堆栈中 上述简单方法的问题在于,它需要大量容易出错的样板工作 我知道序列化(保存/加载)部分的工作可以通过使用Google的Protocol Buffers或Apach

我想制作一个应用程序,用户可以编辑一个图表(例如),它将提供标准的机制:保存、加载、撤消和重做

一个简单的方法是为图表和其中的各种形状创建类,这些类通过save和load方法实现序列化,所有编辑它们的方法都返回
UndoableAction
s,可以将其添加到
UndoManager
中,后者调用它们的
perform
方法并将它们添加到撤销堆栈中

上述简单方法的问题在于,它需要大量容易出错的样板工作

我知道序列化(保存/加载)部分的工作可以通过使用Google的Protocol Buffers或Apache Thrift之类的工具来解决,它可以为您生成锅炉板序列化代码,但它不能解决撤消+重做的问题。我知道,对于目标C和SWIFT,苹果提供了核心数据,它解决了序列化+撤销,但我不熟悉任何类似的C++。
有没有一种不容易出错的好方法,可以用小样本解决save+load+undo+redo问题?

假设您正在为图表的每次编辑对临时文件调用save()(即使用户没有显式调用save操作),并且您只撤消最新的操作,您可以执行以下操作:

LastDiagram load(const std::string &path)
{
  /* Check for valid path (e.g. boost::filesystem here) */
  if(!found)
  {
    throw std::runtime_exception{"No diagram found"};
  } 
  //read LastDiagram
  return LastDiagram;
}
LastDiagram undoLastAction()
{
    return loadLastDiagram("/tmp/tmp_diagram_file");
}
在主应用程序中,如果抛出异常,您将处理该异常。如果您想允许更多的撤销,那么您应该考虑使用类似sqlite或包含更多条目的tmp文件的解决方案

如果大型图表在时间和空间上存在性能问题,请考虑实施一些策略,例如在std::vector中为图表的每个元素保留增量差异(如果对象较大,则将其限制为3/5),并使用当前状态调用渲染器。我不是OpenGL专家,但我认为这就是它的工作方式。实际上,你可以从游戏开发的最佳实践中“窃取”这个策略,或者通常是图形相关的

其中一种策略可能是这样的:


框架和中实现了解决此问题的两种合理方法

代码生成/ODB 使用ODB,您需要向代码中添加
#pragma
声明,并使用其工具生成用于保存/加载和编辑模型的方法,如下所示:

#pragma db object
class person
{
public:
    void setName (string);
    string getName();
    ...
private:
    friend class odb::access;
    person () {}

    #pragma db id
    string email_;

    string name_;
 };
其中,类中声明的访问器由ODB自动生成,因此可以捕获对模型的所有更改,并且可以为它们执行撤消事务

使用最小样板/翻转进行反射

不像ODB,翻转不会为你生成C++代码,而是需要你的程序调用<代码>模型::声明< /C> >重新声明你的结构,如:

class Song : public flip::Object
{
public:
    static void declare ();
    flip::Float tempo;
    flip::Array <Track> tracks;
};

void Song::declare ()
{
    Model::declare <Song> ()
    .name ("acme.product.Song")
    .member <flip::Float, &Song::tempo> ("tempo");
    .member <flip::Array <Track>, &Song::tracks> ("tracks");
}

int main()
{
    Song::declare();
    ...
}
类歌曲:公共翻转::对象
{
公众:
静态void声明();
翻转:浮动节奏;
翻转::阵列轨迹;
};
无效歌曲::声明()
{
模型::声明()
.name(“acme.product.Song”)
.成员(“节奏”);
.成员(“轨道”);
}
int main()
{
歌曲::declare();
...
}
对于这样声明的结构化对象,
flip::Object
的构造函数可以初始化所有字段,以便它们可以指向撤销堆栈,并记录对它们所做的所有编辑。它还有一个所有成员的列表,以便
flip::Object
可以为您实现序列化

上述简单方法的问题在于,它需要大量容易出错的样板工作

我不相信这是事实。你的方法听起来合理,使用现代C++特性和抽象,你可以为它实现一个安全优雅的界面。 对于初学者,您可以将其用作“可撤消操作”的求和类型,这将为每个操作提供一个类型安全的标记联合。(考虑使用
boost::variant
或其他实现,如果您没有访问C++17的权限,可以在Google上轻松找到这些实现)。例如:

namespace action
{
    // User dragged the shape to a separate position.
    struct move_shape
    {
        shape_id _id;
        offset _offset;
    };

    // User changed the color of a shape.
    struct change_shape_color
    {
        shape_id _id;
        color _previous;
        color _new;
    };

    // ...more actions...
}

using undoable_action = std::variant<
    action::move_shape,
    action::change_shape_color,
    // ...
>;
如果
match
的每个分支都变大,则可以将其移动到自己的函数中以提高可读性。尝试实例化
undo\u not\u implemented
或使用依赖的
static\u assert
也是一个好主意:如果忘记实现特定“可撤销操作”的行为,将产生编译时错误

差不多就是这样!如果要保存
undo_堆栈
,以便在保存的文档中保留操作的历史记录,则可以实现
自动序列化(const undoable_action&)
,再次使用模式匹配来序列化各种操作。然后可以实现一个
反序列化
函数,在文件加载时重新填充
undo_堆栈

如果您发现对每一个动作执行序列化/反序列化太繁琐,请考虑使用或类似的解决方案来自动生成序列化/反序列化代码。

由于您关心电池和性能,我还想指出,与多态层次结构相比,使用
std::variant
或类似的带标记的联合构造平均更快、更轻,因为不需要堆分配,也不需要运行时
虚拟
调度


关于
redo
功能:您可以拥有一个
redo\u堆栈
,并实现一个
自动反转(const undobable\u action&)
功能,该功能反转操作的行为。例如:

void undo()
{
    auto action = undo_stack.pop_and_get();
    match(action, [&](const move_shape& y)
                  {
                      // Revert shape movement.
                      shapes[y._id].move(-y._offset);
                      redo_stack.push(invert(y));  
                  },
                  // ...

如果遵循此模式,则可以在
撤消
方面实现
重做
!只需从
redo_堆栈
而不是
undo_堆栈
弹出调用
undo
:由于您“反转”了操作,它将执行所需的操作


EDIT:这里有一个函数,它实现了一个
match
函数,该函数接受一个变量并返回一个变量

  • 该示例使用
    boost::h
    
    void undo()
    {
        auto action = undo_stack.pop_and_get();
        match(action, [&](const move_shape& y)
                      {
                          // Revert shape movement.
                          shapes[y._id].move(-y._offset);
                          redo_stack.push(invert(y));  
                      },
                      // ...
    
    auto invert(const undoable_action& x)
    {
        return match(x, [&](move_shape y)
                    {
                        y._offset *= -1;
                        return y;
                    },
                    // ...
    
    #include <memory>
    #include <stack>
    #include <vector>
    #include <utility>
    #include <iostream>
    #include <algorithm>
    #include <string>
    
    struct Serializer;
    
    struct Part {
        virtual void accept(Serializer &) = 0;
        virtual void draw() = 0;
    };
    
    struct Node: Part {
        void accept(Serializer &serializer) override;
        void draw() override;
        std::string label;
        unsigned int x;
        unsigned int y;
    };
    
    struct Link: Part {
        void accept(Serializer &serializer) override;
        void draw() override;
        std::weak_ptr<Node> from;
        std::weak_ptr<Node> to;
    };
    
    struct Serializer {
        void visit(Node &node) {
            std::cout << "serializing node " << node.label << " - x: " << node.x << ", y: " << node.y << std::endl;
        }
    
        void visit(Link &link) {
            auto pfrom = link.from.lock();
            auto pto = link.to.lock();
           std::cout << "serializing link between " << (pfrom ? pfrom->label : "-none-") << " and " << (pto ? pto->label : "-none-") << std::endl;
        }
    };
    
    void Node::accept(Serializer &serializer) {
        serializer.visit(*this);
    }
    
    void Node::draw() {
        std::cout << "drawing node " << label << " - x: " << x << ", y: " << y << std::endl;
    }
    
    void Link::accept(Serializer &serializer) {
        serializer.visit(*this);
    }
    
    void Link::draw() {
        auto pfrom = from.lock();
        auto pto = to.lock();
    
        std::cout << "drawing link between " << (pfrom ? pfrom->label : "-none-") << " and " << (pto ? pto->label : "-none-") << std::endl;
    }
    
    struct TreeDiagram;
    
    struct Command {
        virtual void execute(TreeDiagram &) = 0;
        virtual void undo(TreeDiagram &) = 0;
    };
    
    struct TreeDiagram {
        std::vector<std::shared_ptr<Part>> parts;
        std::stack<std::unique_ptr<Command>> commands;
    
        void execute(std::unique_ptr<Command> command) {
            command->execute(*this);
            commands.push(std::move(command));
        }
    
        void undo() {
            if(!commands.empty()) {
                commands.top()->undo(*this);
                commands.pop();
            }
        }
    
        void draw() {
            std::cout << "draw..." << std::endl;
            for(auto &part: parts) {
                part->draw();
            }
        }
    
        void serialize(Serializer &serializer) {
            std::cout << "serialize..." << std::endl;
            for(auto &part: parts) {
                part->accept(serializer);
            }
        }
    };
    
    struct AddNode: Command {
        AddNode(std::string label, unsigned int x, unsigned int y):
            label{label}, x{x}, y{y}, node{std::make_shared<Node>()}
        {
            node->label = label;
            node->x = x;
            node->y = y;
        }
    
        void execute(TreeDiagram &diagram) override {
            diagram.parts.push_back(node);
        }
    
        void undo(TreeDiagram &diagram) override {
            auto &parts = diagram.parts;
            parts.erase(std::remove(parts.begin(), parts.end(), node), parts.end());
        }
    
        std::string label;
        unsigned int x;
        unsigned int y;
        std::shared_ptr<Node> node;
    };
    
    struct AddLink: Command {
        AddLink(std::shared_ptr<Node> from, std::shared_ptr<Node> to):
            link{std::make_shared<Link>()}
        {
            link->from = from;
            link->to = to;
        }
    
        void execute(TreeDiagram &diagram) override {
            diagram.parts.push_back(link);
        }
    
        void undo(TreeDiagram &diagram) override {
            auto &parts = diagram.parts;
            parts.erase(std::remove(parts.begin(), parts.end(), link), parts.end());
        }
    
        std::shared_ptr<Link> link;
    };
    
    struct MoveNode: Command {
        MoveNode(unsigned int x, unsigned int y, std::shared_ptr<Node> node):
            px{node->x}, py{node->y}, x{x}, y{y}, node{node}
        {}
    
        void execute(TreeDiagram &) override {
            node->x = x;
            node->y = y;
        }
    
        void undo(TreeDiagram &) override {
            node->x = px;
            node->y = py;
        }
    
        unsigned int px;
        unsigned int py;
        unsigned int x;
        unsigned int y;
        std::shared_ptr<Node> node;
    };
    
    int main() {
        TreeDiagram diagram;
        Serializer serializer;
    
        auto addNode1 = std::make_unique<AddNode>("foo", 0, 0);
        auto addNode2 = std::make_unique<AddNode>("bar", 100, 50);
        auto moveNode2 = std::make_unique<MoveNode>(10, 10, addNode2->node);
        auto addLink = std::make_unique<AddLink>(addNode1->node, addNode2->node);
    
        diagram.serialize(serializer);    
        diagram.execute(std::move(addNode1));
        diagram.execute(std::move(addNode2));
        diagram.execute(std::move(addLink));
        diagram.serialize(serializer);
        diagram.execute(std::move(moveNode2));
        diagram.draw();
        diagram.undo();
        diagram.undo();
        diagram.serialize(serializer);
    }