在不破坏 C++ 抽象的情况下处理对存储在私有映射中的值的封装访问的标准方法

Standard way to handle the encapsulated access to values stored in private map without breaking the abstraction in C++

我想创建一个 class 以便在 C++ 中管理标记语言(例如 HTML)。我希望我的 class 保留属性和子标签。问题是,给定封装的容器,如何正确抽象访问以及 return 的内容,以便提供一种简单的方法来检查值 returned 是否有效。

我定义了我的 class 包含两个映射作为私有成员(名义上,std::map<std::string, Tag> _children;std::map<std::string, std::string> _attr;。我定义了两个函数来填充这些字段,我想定义两个函数读取访问存储的元素。

问题是,我不想破坏我的抽象,因为我这样做是为了提高我的 c++ 技能,我想找到正确的方法(或更简洁的方法,或标准的方法)方式)来做到这一点。

一个基本的解决方案是简单地调用 return map.find(s);,但是我必须将函数的 return 类型定义为 std::map<std::string, Tag>::const_iterator,这会破坏抽象。所以我可以取消引用由 map.find() 编辑的迭代器 return,但如果值不在地图中,我将取消引用不可取消引用的迭代器 (_children.cend()).

到目前为止我定义的内容:

using namespace std;
class Tag {
    static const regex re_get_name, re_get_attributes;
    string _name;
    map<string,string> _attr;
    map<string,Tag> _children;
    public:
        Tag(const string &toParse) {
            /* Parse line using the regex */
        }
        const string& name() const {
            return _name;
        }
        Tag& add_child(const Tag& child) {
            _children.insert(child._name, child);
            return *this;
        }
        SOMETHING get_child(const string& name) const {
            map<string,Tag>::const_iterator val = _children.find(name);
            /* Do something here, but what ? */
            return something;
        }
        SOMETHING attr(const string& name) const {
            map<string, string>::const_iterator val = _attr.find(name);
            /* Do something here, but what ? */
            return something;
        }
};

const regex Tag::re_get_name("^<([^\s]+)");
const regex Tag::re_get_attributes(" ([^\s]+) = \"([^\s]+)\"");

在 C++ 中处理这种情况的正确方法是什么?我应该创建自己的 Tag::const_iterator 类型吗?如果是这样,怎么办?我是否应该采用更 "C" 的方法,如果地图不包含我的密钥,我只将 return 类型定义为 Tag& 和 return NULL ?如果元素不在我的地图中,我是否应该使用静态成员 static const Tag NOT_FOUND 和对此对象的引用 return 更多的 OOP?我也想过抛出异常,但是异常管理在C++中似乎相当繁重且无效。

std::optional 可以帮助你,但需要一个 C++17 就绪的标准库,所以与此同时你也可以使用 boost::optional 这或多或少是一样的,因为 AFAIK std::optionals 的设计是基于 boost 的。 (因为 boost 通常是新 C++ 标准提案的来源)

由于你的方法普遍存在问题,我不愿意给你一个建议,我还是给你写了一个,但请考虑代码后的要点:

#include <string>
#include <regex>
#include <map>
#include <boost/optional.hpp>

class Tag {
    static const std::regex re_get_name, re_get_attributes;
    using string = std::string;
    string _name;
    std::map<string,string> _attr;
    std::map<string,Tag> _children;
    public:
        Tag(const string &toParse) {
            /* Parse line using the regex */
        }
        const string& name() const {
            return _name;
        }
        Tag& add_child(const Tag& child) {
            _children.emplace(child._name, child);
            return *this;
        }
        boost::optional<Tag> get_child(const string& name) const {
            auto val = _children.find(name);

            return val == _children.cend() ? boost::optional<Tag>{} : boost::optional<Tag>{val->second};
        }
        boost::optional<string> attr(const string& name) const {
            auto val = _attr.find(name);

            return val == _attr.cend() ? boost::optional<string>{} : boost::optional<string>{val->second};
        }
};

如您所见,您基本上只是在重新实现 std::map 的容器语义,而且还使用某种内置的解析器逻辑。我强烈反对这种方法,因为解析很快变得非常丑陋,并且将值生成代码混合到一个容器中,即应该用作值 class 会使事情变得更糟。

我的第一个建议是 declare/use 你的 Tag class/struct 作为值 class,所以只包含 std::maps 作为 [=36= 】 成员。将您的解析函数与 Tag 容器一起放在命名空间中,如果需要,让它们只是函数或不同的 classes。

我的第二个建议是小建议:不要用_作为前缀,它是保留的并且被认为是不好的风格,但你可以将它用作后缀。也不要在 class/function/namespace 块之外使用命名空间指令,即全局,它在 .cpp 中是糟糕的风格,在 header /.h/.hpp

中是非常糟糕的风格

我的第三个建议:使用 boost spirit qi 解析器框架,你只需按照我的建议首先声明你的值 classes,而 qi 会通过 boost fusion 自动填充它们。如果你已经知道 EBNF 表示法,你可以在 C++ 中编写类似 EBNF 的语法,编译器将通过模板魔法生成一个解析器。然而,气,尤其是融合存在一些问题,但它使事情变得容易得多 运行。正则表达式最多只完成一半的解析逻辑。