Является ли статический анализатор клана путаным, выбирая фронт из списка unique_ptrs?
Следующий код С++ 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 утверждает, что
... filePath.../main.cpp: 21: 29: Использование памяти после ее освобождения (в пределах вызова "начать" )
Насколько я понимаю, этот код безвреден, но 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
Ответы
Ответ 1
Это признано ошибкой команды 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
Код, как он есть, выглядит отлично.
Я проверяю код из libС++ (соответствующие части), и я считаю, что он просто путает статический анализатор.
Подробнее:
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_ |--
+---------+ +---------+ +---------+ +---------+
Чтобы удалить диапазон A
- B
из этого списка:
-
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_
является idempotent (потому что __end_.prev
сам __end_
).
Возможно, это:
- анализатор учитывает случай сложения пустого списка (
_LIBCPP_ASSERT
)
- и заключает, что в этом случае
__end_.__next_
(используемый begin()
) остается висящим вызовом deallocate()
в pop_front()
Или, может быть, это что-то еще в танце стрелка... надеюсь, команда Клана сможет исправить ситуацию.