Аннулирование удаленных указателей?

#c #pointers #memory-management #invalidation

#c #указатели #управление памятью #аннулирование

Вопрос:

 template<typename T>
someclass<T>amp; operator=(const someclass<T>amp; other)
{
    typename std::vector<T *>::const_iterator rhs;
    typename std::vector<T *>::iterator lhs;

    //identity test
    //this->data is std::vector<T *>

    for(lhs = this->data.begin(); lhs != this->data.end(); lhs  )
    {
        delete *lhs;
    }

    this->data.clear(); // this is what I forgot

    this->data.reserve(other.data.size());
    for (rhs = other.data.begin(); rhs != other.data.end(); rhs  )
    {
        if (NULL == *rhs)
        {
            this->data.push_back(NULL);
        }
        else
        {
            this->data.push_back(new T(**rhs));
        }
    }
}
  

Как вы можете видеть в комментариях, я забыл очистить старые указатели в массиве. Когда я вызвал оператор присваивания во второй раз, я получил ошибку glibc с жалобой на double free. Единственной предоставленной информацией был удаленный адрес.

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

Что было бы хорошо для отладки, так это некоторое значение, например INVALID , которое вы присваиваете этим указателям, говоря: «вызов delete по этому указателю является ошибкой», вместо NULL, в котором говорится: «вызов delete по этому указателю ничего не делает». Есть ли что-то подобное?

Комментарии:

1. Это будет не очень надежно — вы можете легко ошибиться в коде аннулирования и пропустить ошибки, которые вы пытаетесь обнаружить. Инструмент динамического анализа, такой как Valgrind , надежно фиксирует двойные удаления и многие другие ошибки памяти. (И, конечно, как говорится в ответах, отказ от ручного управления памятью в первую очередь предотвратит такие ошибки)

2. Майк Сеймур: верно, но предположим, что я поставил *lhs = 0 после удаления. Все было бы хорошо, но у меня была бы утечка памяти, которую даже valgrind не находит

3. Да, если вы намеренно замаскируете ошибку, вам будет сложнее ее найти. Зачем вам это делать?

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

5. @Dadam: установка указателя на NULL может быть сомнительной практикой. Что бы это сделало здесь? Заставьте контейнер расти бесконечно. Если пользовательский код исправно проверяет значение NULL, в результате у вас может быть программа, которая работает нормально (на данный момент), но в некотором смысле утечка памяти (память никогда не освобождается, а использование постоянно увеличивается).

Ответ №1:

Нет. Лучшей идеей было бы не использовать необработанные указатели, если вы хотите иметь семантику владения. Если вы зададите тип data be boost::ptr_vector<T> или std::vector<std::unique_ptr<T>> then, вам не придется вручную управлять временем жизни ваших указателей, и проблема исчезнет.

Ваш контейнер неправильно поддерживает полиморфные объекты, поскольку представленный вами оператор присваивания будет разрезать объекты в контейнере, когда они будут назначены другому контейнеру. Еще лучшим решением может быть просто иметь std::vector<T> . Это было бы уместно только в том случае, если бы вы не рассчитывали на какое-то другое свойство контейнеров указателей (например, недопустимость признания недействительными указателей на элементы или потенциально более быстрые операции сортировки).

Комментарии:

1. Вы должны использовать какой-то интеллектуальный указатель, если объекты, на которые указывают, должны принадлежать std::vector . В противном случае такие вещи, как исключение из push_back , приведут к утечкам памяти.

2. @James Kanze: я почти уверен, что можно не использовать интеллектуальные указатели и по-прежнему писать код, защищенный от исключений. Это просто означало бы много catch(...) блоков и избыточный код очистки. Но, нет, это не очень хорошая идея!

3. Я не могу использовать boost там, иначе я бы с радостью использовал unique_ptr и выбросил деструктор. Для нарезки: это внутренний класс, который обрабатывает неполиморфные структуры

4. @Mankarse Я подозреваю, что это теоретически возможно. Возможно при разумном времени разработки или с программистами-людьми, которые подвержены ошибкам, я не знаю. Я подозреваю, что для этого потребуется достаточно catch блоков, чтобы вы фатально пропустили один :-).

Ответ №2:

Решение этой проблемы заключается в написании кода, который не содержит никаких удалений. Используйте shared_ptr там, где это возможно. Если у вас есть контейнер, которому принадлежат полиморфные объекты, вы также можете использовать контейнер указателей.

Комментарии:

1. Нет, нет, нет! Не используйте shared_ptr . Нет причин желать предоставить совместное владение элементам частного элемента данных. Использование shared_ptr приведет к обфускации времени жизни объектов в коде и, вероятно, приведет к снижению производительности.

2. @Mankarse: в C 03 это более или менее ваш единственный вариант, если вы хотите, чтобы объект принадлежал стандартному контейнеру. (В C 11 unique_ptr предпочтительнее по причинам, которые вы указываете, но это не вариант без семантики перемещения.)

3. @Mankarse: я еще не добрался до C 11-land 🙂

4. @Mike Seymore: нет причин ограничивать себя стандартными контейнерами. Повышение. Перемещение можно использовать для создания разумного unique_ptr (в C 03), который затем можно использовать в контейнерах из предлагаемого Boost. Библиотека контейнеров . Существует также Boost. Контейнер указателей , который, вероятно, является лучшим решением.

5. Но при их отсутствии это shared_ptr разумный вариант. По крайней мере, код не будет неправильным.