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

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

У меня есть очень общий 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/и т.д.

Наследование C++ иногда кажется правильным, но обычно кажется неправильным. Я чувствую, что вместо наследования у меня должны быть классы, которые существуют независимо от Node, и просто каким-то образом взаимодействуют с ним как «помощники доступа»… но я не очень хорошо понимаю, на что это похоже.


person HostileFork says dont trust SE    schedule 23.06.2012    source источник


Ответы (2)


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

Вы определенно на правильном пути в отношении наследования. Все узлы UL, узлы LI и т. д. являются Node-s. Идеальные отношения «is_a», вы должны получить эти классы от класса Node.

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

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

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

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

Это не всегда может быть ошибкой С++ :-)

person Laryx Decidua    schedule 23.06.2012
comment
Было бы иначе, если бы это было построено на системе сериализации, в которой дерево объектов знало бы, как записывать себя, считывать себя обратно и запускать правильные конструкторы. Поскольку сериализации нет, Node на самом деле создается только во время навигации по дереву... он инкапсулирует дескриптор, а не правильный объект C++. Таким образом, вместо того, чтобы представлять структуру, построенную правильно типизированными конструкторами, она всегда создается одним и тем же базовым классом дескриптора. Это означает, что невозможно dynamic_cast...данные хранятся в базе данных и упоминаются только по ссылке. :-/ Есть смысл? - person HostileFork says dont trust SE; 24.06.2012
comment
Как ни странно, в Википедии есть статья специально для Object-relational-model-impedance-mismatch, где они отражают некоторые из моих опасений по поводу утверждений, таких как основные концепции ООП для классов объектов, наследование и полиморфизм, которые не поддерживаются системами реляционных баз данных. Моя база данных представляет собой графовую базу данных MMF, но даже при этом она не может зафиксировать идиоматическое наследование С++, которое вы описываете. Я нахожу много да, вы облажались страницы, но интересно, есть ли у кого-нибудь новые идеи, которые я еще не видел для этого случая. :) - person HostileFork says dont trust SE; 24.06.2012
comment
После постепенного изучения этого вопроса в течение почти месяца я написал свой собственный ответ. Он взят из находок из нескольких источников и может представлять для вас интерес, а может и не представлять...! - person HostileFork says dont trust SE; 14.07.2012

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

Действительно, и это заявление настораживает:

То, что делает узел «может бытьCast» в UnorderedListNode, не имеет ничего общего с vtables/etc.

Как и этот код:

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

(1) type -каламбур

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

(2) strict -псевдоним

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

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

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

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

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

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

Используя термины шаблона проектирования, это соответствует чему-то вроде Proxy. У вас будет легкий объект, который хранит указатель, а затем передается по значению. На практике может быть сложно справиться с такими проблемами, как что делать с 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 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 () { }
};

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

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() { }
};

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


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

person HostileFork says dont trust SE    schedule 14.07.2012