Извлечение из базового класса, экземпляры которого находятся в фиксированном формате (база данных, 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;
}
Если Node&
было передано через механизм, который юридически и должным образом не UnorderedListNode
на пути наследования это то, что называется типом наказания. Это почти всегда плохая идея. Даже если структура памяти большинства компиляторов работает, когда нет виртуальных функций, а производные классы не добавляют членов данных, они могут разбить ее в большинстве случаев.
Далее следует предположение компилятора о том, что указатели на объекты принципиально разных типов не "псевдоним" друг друга. Это строгое требование алиасинга. Хотя его можно отключить с помощью нестандартных расширений, его следует применять только в устаревших ситуациях... это препятствует оптимизации.
С академической точки зрения не совсем ясно, имеют ли эти два препятствия обходные пути, разрешенные спецификацией при особых обстоятельствах. Вот вопрос, который исследует это, и все еще является открытым обсуждением на момент написания:
Но процитировать @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++:-)