Шаблон нулевого объекта, рекурсивный класс и предварительные объявления - PullRequest
1 голос
/ 30 ноября 2009

Я заинтересован в том, чтобы сделать что-то вроде следующего, чтобы придерживаться шаблона проектирования Null Object и избежать тщательных тестов NULL:

class Node;
Node* NullNode;

class Node {
public:
  Node(Node *l=NullNode, Node *r=NullNode) : left(l), right(r) {};
private:
  Node *left, *right;
};

NullNode = new Node();

Конечно, как написано, NullNode имеет разные области памяти до и после объявления класса Node. Вы можете сделать это без предварительного объявления, если вы не хотите иметь аргументы по умолчанию (т.е. удалить Node * r = NullNode).

Другой вариант мог бы использовать наследование: создать родительский класс (Node) с двумя дочерними элементами (NullNode и FullNode). Тогда приведенный выше пример узла будет кодом для FullNode, а NullNode в приведенном выше коде будет иметь тип NullNode, наследуемый от Node. Я ненавижу решать простые проблемы путем обращения к наследству.

Итак, вопрос в том, как применить шаблоны Null Object к рекурсивным структурам данных (классам) с аргументами по умолчанию (которые являются экземплярами того же класса!) В C ++?

Ответы [ 3 ]

3 голосов
/ 03 марта 2011

Я фактически реализовал рекурсивное дерево (для JSON и т. Д.), Выполняя что-то вроде этого. По сути, ваш базовый класс становится реализацией «NULL», а его интерфейс представляет собой объединение всех интерфейсов для производного. Затем у вас есть производные классы, которые реализуют части - DataNode реализует методы получения и установки данных и т. Д.

Таким образом, вы можете запрограммировать интерфейс базового класса и сэкономить ОЧЕНЬ много боли. Вы настроили базовую реализацию, чтобы выполнить всю шаблонную логику за вас, например,

class Node {
    public:
    Node() {}
    virtual ~Node() {}

    virtual string OutputAsINI() const { return ""; }
};

class DataNode {
    private:
    string myName;
    string myData;

    public:
    DataNode(const string& name, const string& val);
    ~DataNode() {}

    string OutputAsINI() const { string out = myName + " = " + myData; return out; }
};

Таким образом, мне не нужно ничего проверять - я просто слепо называю "OutputAsINI()". Подобная логика для всего вашего интерфейса отбросит большинство нулевых тестов.

3 голосов
/ 30 ноября 2009

Использование extern:

extern Node* NullNode;
...
Node* NullNode = new Node();

Еще лучше, сделайте его статическим членом:

class Node {
public:
  static Node* Null;
  Node(Node *l=Null, Node *r=Null) : left(l), right(r) {};
private:
  Node *left, *right;
};

Node* Node::Null = new Node();

Тем не менее, как в существующем коде, так и в приведенных выше изменениях, вы пропускаете экземпляр Node. Вы могли бы использовать auto_ptr, но это было бы опасно из-за неопределенного порядка уничтожения глобалов и статики (для деструктора некоторого глобала может потребоваться Node::Null, и к тому времени он может уже исчезнуть или не исчезнуть).

0 голосов
/ 30 ноября 2009

Инвертировать иерархию. Поместите нулевой узел в основание:

class Node {
public:
  Node() {}
  virtual void visit() const {}
};

Затем специализируйтесь по мере необходимости:

template<typename T>
class DataNode : public Node {
public:
  DataNode(T x, const Node* l=&Null, const Node* r=&Null)
    : left(l), right(r), data(x) {}

  virtual void visit() const {
    left->visit();
    std::cout << data << std::endl;
    right->visit();
  }

private:
  const Node *left, *right;
  T data;
  static const Node Null;
};

template<typename T>
const Node DataNode<T>::Null = Node();

Пример использования:

int main()
{
  DataNode<char> a('A', new DataNode<char>('B'),
                        new DataNode<char>('C'));

  a.visit();

  return 0;
}

Выход:

$ ./node 
B
A
C
...