Безопасно ли здесь делать конст-бросок?

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

Это заголовочный файл для итератора DFS, который я написал:

template<class Item>
class DFSIterator
{
public:
    DFSIterator(const Item& rRootNode);
    ~DFSIterator();
    DFSIterator* First();
    DFSIterator* operator++(int rhs);
    Item* operator*() const;
    Item* operator->() const;
    bool isDone() const;

    template <class Node> friend class Node;

private:
    void initListIterator(const Item* currentNode);

    bool m_bIsDone;
    const Item* m_pRootNode;
    const Item* m_pCurrentNode;
    ListIterator<Item>* m_pCurrentListIter;
    std::map<const Item*, ListIterator<Item>*>  m_listMap;
};

Таким образом, меня беспокоит то, что оператор разыменования:

template<class Item>
Item* DFSIterator<Item>::operator*() const
{
    if(isDone())
    {
        return NULL;
    }
    else
    {
        return const_cast<Item*>(m_pCurrentNode);
    }
}

Это уместно сделать const_cast там? Мне интересно, если это вызовет проблемы, если пользователь помещает const объекты в контейнер?

5 ответов

Решение

const_cast сам по себе всегда безопасен, но любая попытка написать const_cast ценности, которые были объявлены const является неопределенным поведением.

В этом случае, если ваш возвращенный указатель указывает на объявленное значение const и вы попытаетесь изменить его, вы получите неопределенное поведение.


Правило большого пальца: если вам нужно использовать const_cast для всего, кроме взаимодействия с некорректным кодом, ваш дизайн не работает.


Стандарт гласит в 7.1.6.1. Спецификаторы cv:

За исключением того, что любой член класса, объявленный mutable (7.1.1), может быть изменен, любая попытка изменить объект const в течение его времени жизни (3.8) приводит к неопределенному поведению.

Поскольку ваш конструктор использует const ItemВаш оператор должен вернуть константный указатель.

Если вы хотите вернуть неконстантный элемент, вы должны использовать неконстантный параметр для вашего конструктора. Решение состоит в том, чтобы иметь базовый класс, использующий константные объекты, и подкласс, работающий с неконстантными объектами (немного по-другому, например, в NSString и NSMutableString).

Не выбрасывайте const, так как это нарушит его значение! Есть причина, по которой в STL есть класс const_iterator и итератор. Если вам нужна постоянная корректность, вам нужно реализовать два отдельных класса итераторов. Одна из целей итератора - имитировать указатель сохранения. Таким образом, вы не хотите возвращать нуль, если вы находитесь за пределами диапазона итераций, вы хотите поднять отладочное утверждение, если это действительно происходит.

Класс константного итератора может выглядеть примерно так:

template<class Item>
class DFSIteratorConst
{
public:
    DFSIteratorConst(const Item& node)
    {
        m_pCurrentNode = &node;
    };

    const Item& operator*() const
    {
        assert(!IsDone());
        return *m_pCurrentNode;
    }

    void operator++()
    {
        assert(!IsDone());
        m_pCurrentNode = m_pCurrentNode->next;
    }

    operator bool() const
    {
        return !IsDone();
    }

    bool IsDone() const
    {
        return m_pCurrentNode == nullptr;
    }

private:
    Item const * m_pCurrentNode;
};

Несколько вещей, на которые стоит обратить внимание:

  • рассмотрите возможность возврата ссылки на узел (константная ссылка). Это приводит к тому, что вы не сможете вернуть ноль, если итератор находится за последним элементом. Разыменование прошлого итератора конца обычно является плохим поведением, и вы хотите найти эти проблемы во время отладочных сборок и тестирования
  • класс константного итератора имеет указатель на элемент const. Это означает, что он может изменить указатель (иначе вы не сможете выполнить итерацию), но не может изменить сам элемент
  • неявное преобразование в bool целесообразно, если вы хотите проверить итератор в цикле while. Это позволяет вам написать: while(it) { it++; } вместо while(!it.IsDone()) { it++; } опять то, что напоминает классический указатель.
  • используйте nullptr, если доступно

Как я могу видеть из вашего использования std::map Вы уже используете STL. Может быть, было бы проще использовать существующие итераторы STL?

Обычно вы пишете две версии итераторов, iterator и const_iterator,

Существуют хитрости шаблонов, позволяющие избежать дублирования кода, описанного в документации библиотеки Boost.Iterator. Посмотрите, как определяется адаптер Iterator, поскольку он довольно длинный.

Дело в том, что вы напишете BaseIterator то есть const сознательно, а затем предоставить псевдонимы:

  • typedef BaseIterator<Item> Iterator;
  • typedef BaseIterator<Item const> ConstIterator;

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

Настоящая проблема заключается в том, что этот код / ​​интерфейс "лжет".

Конструктор говорит: я не изменяю тебе (корень). Но итератор раздает изменяемый элемент.

Чтобы быть "честным", либо конструктор получает изменяемый корень, даже если он не будет изменен в этом классе, либо этот класс не отдает изменяемый элемент.

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

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

Вероятно, вам нужны два шаблона в зависимости от "const"-ностиности данного ребенка

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