Статический анализатор 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 утверждает

...filePath.../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

person Thierry    schedule 02.05.2014    source источник
comment
Вы должны сообщить об этом как о ложном срабатывании. Не уверен, в чем вопрос на самом деле... код (очевидно?) в порядке.   -  person David Rodríguez - dribeas    schedule 02.05.2014
comment
Вот в чем вопрос Я что-то упустил или действительно наткнулся на пропущенный кейс в clang?   -  person Jagannath    schedule 03.05.2014
comment
На самом деле, поскольку эта ошибка возникает у меня при использовании libc++, но не при использовании GNU libstdc++ (начиная с clang-3.5 и gcc-4.9 соответственно в 64-битном Debian), это вполне может быть ошибкой в ​​libc++.   -  person Massa    schedule 07.05.2014
comment
@Massa О, интересная гипотеза. Однако я не могу воспроизвести это, мой libstdc++ не предлагает поддержку С++ 11. Но я добавлю эту информацию в баг-репорт на Bugzilla.   -  person Thierry    schedule 08.05.2014


Ответы (2)


Код в его нынешнем виде выглядит нормально.

Я проверяю код из 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_ |--
  +---------+   +---------+   +---------+   +---------+

Чтобы удалить диапазон 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_ является идемпотентным (поскольку __end_.prev сам является __end_).

Может быть, что:

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

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

person Matthieu M.    schedule 09.05.2014

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

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

[т] анализатор не может рассуждать о внутренней инвариантности [контейнеров, таких как 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
person Thierry    schedule 24.07.2015