Извлечение из базового класса, экземпляры которого находятся в фиксированном формате (база данных, MMF)... как быть в безопасности?

(Примечание: я ищу действительно любые предложения по правильным поисковым терминам, чтобы прочитать об этой категории вопросов. "Объектно-реляционное отображение" пришло мне в голову как место, где я мог бы найти хороший уровень техники... но Я пока не видел ничего подходящего этому сценарию.)

У меня очень общий class Node, который на данный момент можно представить как элемент в дереве DOM. Это не совсем то, что происходит - это графические объекты базы данных в файле отображения памяти. Но аналогия довольно близка для всех практических целей, поэтому я буду придерживаться терминов DOM для простоты.

"Тег", встроенный в узел, подразумевает определенный набор операций, которые вы должны (в идеале) иметь с ним. Прямо сейчас я использую производные классы, чтобы сделать это. Например, если вы пытаетесь представить что-то вроде списка HTML:

<ul>
   <li>Coffee</li>
   <li>Tea</li>
   <li>Milk</li>
</ul>

Базовое дерево будет состоять из семи узлов:

+--UL                       // Node #1
   +--LI                    // Node #2
      +--String(Coffee)     // Node #3 (literal text)
   +--LI                    // Node #4
      +--String(Tea)        // Node #5 (literal text)
   +--LI                    // Node #6
      +--String(Milk)       // Node #7 (literal text)

поскольку getString() это уже примитивный метод на самих узлах, я бы, наверное, только сделал class UnorderedListNode : public Node, class ListItemNode : public Node,

Продолжая эту гипотезу, давайте представим, что я хотел помочь программисту использовать менее общие функции, когда они больше знают о "типе" / тэге Node, который они имеют в своих руках. Возможно, я хочу помочь им со структурными идиомами на дереве, такими как добавление элемента строки в неупорядоченный список или извлечение элементов в виде строки. (Это всего лишь аналогия, поэтому не принимайте процедуры слишком серьезно.)

class UnorderedListNode : public Node {
private:
    // Any data members someone put here would be a mistake!

public:
    static boost::optional<UnorderedListNode&> maybeCastFromNode(Node& node) {
        if (node.tagString() == "ul") {
            return reinterpret_cast<UnorderedListNode&>(node);
        }
        return boost::none;
    }

    // a const helper method
    vector<string> getListAsStrings() const {
        vector<string> result;
        for (Node const* childNode : children()) {
            result.push_back(childNode->children()[0]->getText());
        }
        return result;
    }

    // helper method requiring mutable object
    void addStringToList(std::string listItemString) {
        unique_ptr<Node> liNode (new Node (Tag ("LI"));
        unique_ptr<Node> textNode (new Node (listItemString));
        liNode->addChild(std::move(textNode));
        addChild(std::move(liNode));
    }
};

Добавление членов данных в эти новые производные классы - плохая идея. Единственный способ действительно сохранить любую информацию - это использовать основополагающие процедуры Node (например, addChild позвоните выше или getText) взаимодействовать с деревом. Таким образом, реальная модель наследования - в той мере, в которой она существует - находится вне системы типов C++. Что делает <UL> узел "MaybeCast" в UnorderedListNode не имеет никакого отношения к vtables/etc.

Иногда наследование С ++ выглядит правильно, но обычно оно кажется неправильным. Я чувствую, что вместо наследования у меня должны быть классы, которые существуют независимо от Node, и просто сотрудничать с ним как-то как "помощники по доступу"... но я не совсем понимаю, на что это будет похоже.

2 ответа

Решение

"Наследование в C++ иногда выглядит правильно, но обычно кажется неправильным".

Действительно, и это утверждение вызывает беспокойство

То, что превращает узел "MaybeCast" в UnorderedListNode, не имеет ничего общего с vtables/etc.

Как этот код:

static boost::optional<UnorderedListNode&> maybeCastFromNode(Node& node) {
    if (tagString() == "ul") {
        return reinterpret_cast<UnorderedListNode&>(node);
    }
    return boost::none;
}

(1) типовое наказание

Если Node& было передано через механизм, который юридически и должным образом не UnorderedListNode на пути наследования это то, что называется типом наказания. Это почти всегда плохая идея. Даже если структура памяти большинства компиляторов работает, когда нет виртуальных функций, а производные классы не добавляют членов данных, они могут разбить ее в большинстве случаев.

(2) строгий псевдоним

Далее следует предположение компилятора о том, что указатели на объекты принципиально разных типов не "псевдоним" друг друга. Это строгое требование алиасинга. Хотя его можно отключить с помощью нестандартных расширений, его следует применять только в устаревших ситуациях... это препятствует оптимизации.

С академической точки зрения не совсем ясно, имеют ли эти два препятствия обходные пути, разрешенные спецификацией при особых обстоятельствах. Вот вопрос, который исследует это, и все еще является открытым обсуждением на момент написания:

Сделать взаимозаменяемые типы классов только с помощью приведения указателя, без необходимости выделять какие-либо новые объекты?

Но процитировать @MatthieuM.: "Чем ближе вы подходите к границам спецификаций, тем выше вероятность того, что вы попадете в ошибку компилятора. Поэтому, как инженер, я советую быть прагматичным и избегать интеллектуальных игр со своим компилятором; правы вы или нет не имеет значения: когда вы получаете сбой в производственном коде, вы проигрываете, а не авторы компилятора. "

Это, вероятно, более правильный путь:

Я чувствую, что вместо наследования у меня должны быть классы, которые существуют независимо от Node, и просто сотрудничать с ним как-то как "помощники по доступу"... но я не совсем понимаю, на что это будет похоже.

Используя термины Design Pattern, это соответствует чему-то вроде прокси. У вас будет легкий объект, который хранит указатель и затем передается по значению. На практике это может быть сложно решить такие вопросы, как, что делать с const -очность входящих указателей оборачивается!

Вот пример того, как это можно сделать относительно просто для этого случая. Во-первых, определение базового класса Accessor:

template<class AccessorType> class Wrapper;

class Accessor {
private:
    mutable Node * nodePtrDoNotUseDirectly;
template<class AccessorType> friend class Wrapper;
    void setNodePtr(Node * newNodePtr) {
        nodePtrDoNotUseDirectly = newNodePtr;
    }
    void setNodePtr(Node const * newNodePtr) const {
        nodePtrDoNotUseDirectly = const_cast<Node *>(newNodePtr);
    }
    Node & getNode() { return *nodePtrDoNotUseDirectly; }
    Node const & getNode() const { return *nodePtrDoNotUseDirectly; }

protected:
    Accessor() {}

public:
    // These functions should match Node's public interface
    // Library maintainer must maintain these, but oh well
    inline void addChild(unique_ptr<Node>&& child)) { 
        getNode().addChild(std::move(child));
    }
    inline string getText() const { return getNode().getText(); }
    // ...
};

Затем, частичная специализация шаблона для обработки случая обёртывания "const Accessor", который является сигналом того, что он получит const Node &:

template<class AccessorType>
class Wrapper<AccessorType const> {    
protected:
    AccessorType accessorDoNotUseDirectly;
private:
    inline AccessorType const & getAccessor() const {
        return accessorDoNotUseDirectly;
    }

public:
    Wrapper () = delete;
    Wrapper (Node const & node) { getAccessor().setNodePtr(&node); }
    AccessorType const * operator-> const () { return &getAccessor(); }
    virtual ~Wrapper () { }
};

Оболочка для случая "изменяемого аксессора" наследует свою частичную специализацию шаблонов. Таким образом, наследование обеспечивает правильные приведения и присваивания... запрещающие присваивание const неконстантным, но работающие наоборот:

template<class AccessorType>
class Wrapper : public Wrapper<AccessorType const> {
private:
    inline AccessorType & getAccessor() {
        return Wrapper<AccessorType const>::accessorDoNotUseDirectly;
    }

public:
    Wrapper () = delete;
    Wrapper (Node & node) : Wrapper<AccessorType const> (node) { }
    AccessorType * operator-> () { return &Wrapper::getAccessor(); }
    virtual ~Wrapper() { }
};

Реализация компиляции с тестовым кодом и комментариями, документирующими странные части, находится здесь в Gist.


Источники: @MatthieuM. @PaulGroke

Я не уверен, что полностью понял, что вы собираетесь делать, но вот несколько советов, которые вы могли бы найти полезными.

Вы определенно на правильном пути с наследованием. Все узлы UL, узлы LI и т. Д. Являются узлами. Отличные отношения "is_a", вы должны получить эти классы из класса Node.

давайте представим, что я хотел помочь программисту использовать менее общие функции, когда он знает больше о "типе"/ теге Node, который у них в руках

... и это то, для чего нужны виртуальные функции.

Теперь для maybeCastFromNode метод. Это уныло. Почему ты бы так поступил? Может быть для десериализации? Если да, то я бы рекомендовал использовать dynamic_cast<UnorderedListNode *>, Хотя, скорее всего, вам вообще не понадобится RTTI, если дерево наследования и виртуальные методы хорошо спроектированы.

Иногда наследование С ++ выглядит правильно, но обычно оно кажется неправильным.

Это не всегда ошибка C++:-)

Другие вопросы по тегам