10

以下 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:释放内存后的使用(在调用“开始”时)

据我解释,这段代码是无害的,但 clang 忽略了std::list<T>::pop_front()不仅调用其元素的析构函数,而且还移动std::list<T>::begin(). 替换对pop_frontby的调用pop_back会使分析器警告消失,甚至替换它erase(theList.begin())会使它出现无警告。

我是否遗漏了某些东西,或者我真的偶然发现了一个在 clang 中遗漏的案例?

供参考:这些结果来自 Mac OS X 10.9.2 上的 XCode 5.1.1 (5B1008),

$ 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
4

2 回答 2

5

就目前而言,代码看起来不错。

我检查了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_(ie, Z.__next_) 并使其指向B.__next_(ie, C)
  • B.__next_.__prev_(ie, C.__prev_) 并使其指向A.__prev_(ie, 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_(used by begin()) 被deallocate()调用pop_front()

或者也许它是指针舞中的其他东西......希望 Clang 团队能够修补这些东西。

于 2014-05-09T17:55:29.593 回答
2

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
于 2015-07-24T20:16:12.453 回答