Не перепутан ли статический анализатор clang, выскользнувший из списка unique_ptrs?

Следующий код C ++ 11 является минимальным примером того, что, по моему мнению, приводит к ложному срабатыванию в clang:

#include <iostream>
#include <list>
#include <memory>

class ElementType {};

int main(int argc, const char * argv[]) {
std::list<std::unique_ptr<ElementType>> theList(5);

theList.pop_front();

for (const auto &element: theList) { // (*)
std::cout << "This should be fine." << std::endl;
}

return 0;
}

На строке, отмеченной звездочкой (*), анализатор clang утверждает

…Путь файла…/main.cpp:21:29: использование памяти после ее освобождения (в рамках вызова ‘begin’)

Насколько я понимаю, этот код безопасен, но Clang упускает из виду то, что std::list<T>::pop_front() не только вызывает деструктор своих элементов, но и перемещает std::list<T>::begin(), Замена звонка на pop_front от pop_back отключает предупреждение анализатора и даже заменяет его erase(theList.begin()) делает это без предупреждения.

Я что-то упустил или я действительно наткнулся на пропущенный случай в Clang?

Для справки:
Эти результаты получены из XCode 5.1.1 (5B1008) в Mac OS X 10.9.2,

$ clang --version
Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn)
Target: x86_64-apple-darwin13.1.0
Thread model: posix

10

Решение

Код, как он выглядит, выглядит хорошо.

Я проверяю код из libc ++ (соответствующие части), и я считаю, что это просто смущает статический анализатор.

Более подробно:

template <class _Tp, class _Alloc>
void list<_Tp, _Alloc>::pop_front()
{
_LIBCPP_ASSERT(!empty(), "list::pop_front() called with empty list");
__node_allocator& __na = base::__node_alloc();
__node_pointer __n = base::__end_.__next_;
base::__unlink_nodes(__n, __n);
--base::__sz();
__node_alloc_traits::destroy(__na, _VSTD::addressof(__n->__value_));
__node_alloc_traits::deallocate(__na, __n, 1);
}

list реализован в виде кругового списка, основанного на __end_ (который является указателем конца), поэтому, чтобы добраться до первого элемента, код идет в __end_.__next_,

Реализация __unlink_nodes является:

// Unlink nodes [__f, __l]
template <class _Tp, class _Alloc>
inline void __list_imp<_Tp, _Alloc>::__unlink_nodes(__node_pointer __f,
__node_pointer __l) noexcept
{
__f->__prev_->__next_ = __l->__next_;
__l->__next_->__prev_ = __f->__prev_;
}

Мы можем легко понять это с помощью некоторого простого искусства ASCII:

       Z             A             B             C
+---------+   +---------+   +---------+   +---------+
--| __prev_ |<--| __prev_ |<--| __prev_ |<--| __prev_ |<-
->| __next_ |-->| __next_ |-->| __next_ |-->| __next_ |--
+---------+   +---------+   +---------+   +---------+

Удалить диапазон AB из этого списка:

  • Z.__next_ должен указать на C
  • C.__prev_ должен указать на Z

Таким образом, вызов __unlink_nodes(A, B) будут:

  • принимать A.__prev_.__next_ (То есть, Z.__next_) и укажите на B.__next_ (То есть, C)
  • принимать B.__next_.__prev_ (То есть, C.__prev_) и укажите на A.__prev_ (То есть, Z)

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

Теперь, однако, обратите внимание, что если list должны были быть пустыми, это не будет работать вообще! Конструктор по умолчанию __list_node_base является:

__list_node_base()
: __prev_(static_cast<pointer>(pointer_traits<__base_pointer>::pointer_to(*this))),
__next_(static_cast<pointer>(pointer_traits<__base_pointer>::pointer_to(*this)))
{}

То есть это относится к себе. В этом случае, __unlink_nodes вызывается с &__end_ (дважды) и не изменит его __end_.__prev_.__next_ = __end_.__next_ идемпотент (потому что __end_.prev является __end_ сам).

Может быть так:

  • анализатор учитывает случай пустого списка (_LIBCPP_ASSERT составляется)
  • и приходит к выводу, что в этом случае __end_.__next_ (использован begin()) болтается deallocate() вызывать pop_front()

Или, может быть, это что-то еще в танце со стрелками … надеюсь, команда Clang сможет исправить ситуацию.

5

Другие решения

Команда LLVM признала это ошибкой.

В комментировать редакцию 211832 заявлено, что с

[T] анализатор не может рассуждать о внутренних инвариантах [контейнеры
такие как std :: vector и std :: list
] что приводит к ложным срабатываниям

анализатор должен

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

Проблема действительно больше не воспроизводится на XCode 6.4 (6E35b) с

$ clang --version
Apple LLVM version 6.1.0 (clang-602.0.53) (based on LLVM 3.6.0svn)
Target: x86_64-apple-darwin14.4.0
Thread model: posix
2