工作中遇到了一个关于关于llvm::ArrayRef
和std::vector
的内存bug,这个bug涉及到llvm::ArrayRef
的实现以及相关的概念,这里做相关介绍。
该bug由[Bash-autocompletion] Add support for static analyzer flags引入,引起的bug见Revert r311552: [Bash-autocompletion] Add support for static analyzer flags,最终由Keep an instance of COFFOptTable alive as long as InputArgList is alive解决。导致该bug的原因是引用了某个局部对象的vector类型成员变量元素的地址,当该局部对象析构时,调用该了成员变量对应vector的析构函数,因此最初引用的地址就失效了,当再次访问该地址时引发了内存错误。
以下细节内容可以跳过
该成员变量最初是llvm::ArrayRef
,而[Bash-autocompletion] Add support for static analyzer flags将其改为了std::vector<>
类型,那么为什么引用llvm::ArrayRef
的元素地址就不存在该问题?在介绍原因之前,先费口舌记录一下该bug的具体场景。
// -------------------------------------------
template<typename T>
class ArrayRef {
private:
/// The start of the array, in an external buffer.
const T *Data = nullptr;
/// The number of elements.
size_type Length = 0;
public:
operator std::vector () const {
return std::vector (Data, Data + Length);
}
};
// --------------------------------------------
static const OptTable::Info InfoTable[] = {
// Option List
};
class OptTable {
public:
struct Info {
// details
};
// Implicit conversion `Array => std::vector` occurred here,
OptTable(ArrayRef OptionInfos) : OptionInfos(OptionInfos) {}
const Info& getInfo(unsigned id) const {
return Options[id - 1];
}
private:
/// \brief The option information table.
std::vector OptionInfos;
/// ...
};
const OptTable* CompilerInvocation::CreateFromArgs() {
// Local object
auto Opts = std::make_unique(InfoTable);
// Reference the address of Opts.OptionInfos[0]
const OptTable::Info *Ptr = Opts.getInfo(1);
// ...
return Ptr;
} // <---- calling `~OptTable()` on `Opts` and `~vector` on `Opts.OptionInfos`
关于上面的代码有一点需要注意,就是llvm::ArrayRef
定义了到std::vector
的类型转换,该类型转换调用了std::vector
的一个构造函数,该构造函数见std::vector::vector中第四类构造函数,如下所示:
template< class InputIt >
vector( InputIt first, InputIt last,
const Allocator& alloc = Allocator() );
该构造函数的介绍如下:
4) Constructs the container with the contents of the range [first, last).
This constructor has the same effect as
- vector(static_cast(first), static_cast(last), a). if InputIt is an integral type. (until C++11)
This overload only participates in overload resolution if InputIt satisfies InputIterator, to avoid ambiguity with the overload (2). (since C++11)
std::vector
由于需要连续存放,且能够满足动态增删的需求,所以其一般都是在堆上分配一块内存,上面的构造函数也不例外,libcxx对其的实现如下,该构造函数会进行数据的拷贝,并为这些数据分配额外的内存。
/**
393 * @brief Builds a %vector from a range.
394 * @param __first An input iterator.
395 * @param __last An input iterator.
396 * @param __a An allocator.
397 *
398 * Create a %vector consisting of copies of the elements from
399 * [first,last).
400 *
401 * If the iterators are forward, bidirectional, or
402 * random-access, then this will call the elements' copy
403 * constructor N times (where N is distance(first,last)) and do
404 * no memory reallocation. But if only input iterators are
405 * used, then this will do at most 2N calls to the copy
406 * constructor, and logN memory reallocations.
407 */
408 #if __cplusplus >= 201103L
409 template410 typename = std::_RequireInputIter<_InputIterator>>
411 vector(_InputIterator __first, _InputIterator __last,
412 const allocator_type& __a = allocator_type())
413 : _Base(__a)
414 { _M_initialize_dispatch(__first, __last, __false_type()); }
415 #else
416 template
417 vector(_InputIterator __first, _InputIterator __last,
418 const allocator_type& __a = allocator_type())
419 : _Base(__a)
420 {
421 // Check whether it's an integral type. If so, it's not an iterator.
422 typedef typename std::__is_integer<_InputIterator>::__type _Integral;
423 _M_initialize_dispatch(__first, __last, _Integral());
424 }
425 #endif
426
所以示例代码中的Ptr
引用的是一块的堆上内存,当std::vector
的析构调用以后,相应的内存也一并被释放了,所以CompilerInvocation::CreateFromArgs
访问的是一块已经释放的内存。
那么为什么返回llvm::ArrayRef
的元素不会触发上述的bug?
为了解释为什么使用llvm::ArrayRef
没有触发上述bug,需要了解llvm::ArrayRef
的实现机制及其背后的设计理念。
/// ArrayRef - Represent a constant reference to an array (0 or more elements
/// consecutively in memory), i.e. a start pointer and a length. It allows
/// various APIs to take consecutive elements easily and conveniently.
///
/// This class does not own the underlying data, it is expected to be used in
/// situations where the data resides in some other buffer, whose lifetime
/// extends past that of the ArrayRef. For this reason, it is not in general
/// safe to store an ArrayRef.
///
/// This is intended to be trivially copyable, so it should be passed by
/// value.
template<typename T>
class ArrayRef {
private:
/// The start of the array, in an external buffer.
const T *Data = nullptr;
/// The number of elements.
size_type Length = 0;
public:
/// Construct an ArrayRef from a single element.
/*implicit*/ ArrayRef(const T &OneElt)
: Data(&OneElt), Length(1) {}
/// Construct an ArrayRef from a pointer and length.
/*implicit*/ ArrayType(const T *data, size_t length)
: Data(data), Length(length) {}
/// Construct an ArrayRef from a range.
ArrayRef(const T *begin, const T *end)
: Data(begin), Length(end - begin) {}
/// Construct an ArrayRef from a std::vector.
template<typename A>
/*implicit*/ ArrayRef(const std::vector &Vec)
: Data(Vec.data()), Length(Vec.size()) {}
/// Construct an ArrayRef from an std::array
template
/*implicit*/ constexpr ArrayRef(const std::array &Arr)
: Data(Arr.data()), Length(N) {}
/// Construct an ArrayRef from a C array
template
/*implicit*/ constexpr ArrayRef(const T (&Arr)[N]) : Data(Arr), Length(N) {}
/// Construct an Array from std::initializer_list
/*implicit*/ ArrayRef(const std::initializer_list &Vec)
: Data(Vec.begin() == Vec.end() ? (T*)nullptr : Vec.begin()),
Length(Vec.size()) {}
};
从llvm::ArrayRef
的注释中可以得到以下几点信息:
llvm::ArrayRef
表示的是一组连续内存区域,核心是start pointer
和length
llvm::ArrayRef
提供了很多简单方便的API供使用llvm::ArrayRef
并不拥有这些数据,这些数据存放在其他buffer中,并且这些buffer的生命周期比llvm::ArrayRef
要长llvm::ArrayRef
对象并不安全std::vector
,std::array
,std::initializer_list
,数组这里需要岔开一下话题介绍一下上述代码中关于C++11两点内容,constexpr constructor
以及std::vector::data
。
The constexpr specifier declares that it is possible to evaluate the value of the function or variable at compile time. Such variables and functions can then be used where only compile time constant expressions are allowed (provided that appropriate function arguments are given).
使用constexpr
修饰普通函数可以理解,就是让函数在compile-time
evaluate该函数并得到其返回值,用constexpr
用于修饰constructor
有什么意义呢?
这里我的理解有是有两个作用:
constexpr variable
,从而可以用在non-type template arguments
, array sizes
等地方,从另一个角度可以认为拥有constexpr construtor
的自定义类型对象可以用来构成constant expression这里有很多概念,例如constant expression
,literal type
,限于自己C++知识的不足,就不胡说了。当然成为constexpr constructor
也是有一定要求的,见Constexpr constructors (C++11)
The category of types that can be used for
constexpr
variables is called literal type. Most notably, literal types include classes that haveconstexpr
constructors, so that values of the type can be initialized callingconstexpr
functions.
相关资料(这些资料提供的信息很有限):
1. Why would you use a constexpr on a constructor?
2. Does specifying constexpr on constructor automatically makes all objects created from it to be constexpr?
std::vector::data
std::vector::data()
是C++11提供的新特性,允许用户直接获取vector第一个元素的地址,然后可以使用addr+offset
的方式访问vector的数据,C++标准已经保证了vector是连续存储的,所以上述使用方式是安全的。在此之前,用户一般都是使用&vector::front()等其他方式。
从注释中我们可以总结出,llvm::ArrayRef
就是给一组连续存放的数据加了一个壳子,这个壳子提供了很多方便使用的API。比如程序中的一个数组,对于数组并没有什么现成的API可以使用,此时我就可以在该数组上套一个llvm::ArrayRef
壳子,然后通过llvm::ArrayRef
的API对该数组做相应的操作,此时存储该llvm::ArrayRef
对象就没有什么意义了,因为数据并不在它这里,并且存储一个“壳子“有什么用呢?
llvm::ArrayRef提供了很多有用的API,这里就不给出了,可以参见llvm/include/llvm/ADT/ArrayRef.h
。
至此我们就可以回答为什么llvm::ArrayRef
不会触发文章开始的bug了,因为llvm::ArrayRef
所引用的数据的lifetime
与ArrayRef的对象没有关系(从代码示例中可以看到OptionInfos
绑定的数据的是static全局对象)。但是这并不能说明llvm::ArrayRef
就是安全的,例如下面的代码就是不安全的:
llvm::ArrayRef Array({1, 2, 3, 4});
Array[0] = 10;
与llvm::Array
有相同功能的是llvm::StringRef
,Purpose of ArrayRef中有一段描述比较精确,如下:
It’s the same idea behind
std::string_view
: to provide a general view to something, without managing it’s lifetime.In the case of
ArrayRef
(which is a terrible name, ArrayView is much better IMHO), it can view other arrays type, including the non-object builtin array(C array).
所以我们将分别介绍llvm::StringRef
和std::string_view
及其背后的设计理念。
llvm::StringRef
的定义如下,可以看到和llvm::ArrayRef
的理念很相似,除了一些API有稍许不同。
/// StringRef - Represent a constant reference to a string, i.e. a character
/// array and a length, which need not be null terminated.
///
/// This class does not own the string data, it is expected to be used in
/// situations where the character data resides in some other buffer, whose
/// lifetime extends past that of the StringRef. For this reason, it is not in
/// general safe to store a StringRef
class StringRef {
private:
/// The start of the string, in an external buffer.
const char *Data = nullptr;
/// The length of the string.
size_t Length = 0;
public:
/// Construct an empty string ref.
/*implicit*/ StringRef() = default;
/// Disable conversion from nullptr. This prevents things like
/// if (S == nullptr)
StringRef(std::nullptr_t) = delete;
/// Construct a string ref from a cstring.
/*implicit*/ StringRef(const char *Str)
: Data(Str), Length(Str ? ::strlen(Str) : 0) {}
/// Construct a string ref from a pointer and length.
/*implicit*/ constexpr StringRef(const char *data, size_t length)
: Data(data), Length(length) {}
/// Construct a string ref from an std::string
/*implicit*/ StringRef(const std::string &Str)
: Data(data), Length(length) {}
};
llvm::StringRef
可以直接使用c string
和std::string
初始化,并且llvm::StringRef
提供了一系列的API,填充了std::string
的不足,例如我想判断某个字符串是否以某个子串结尾,就可以使用endswith()
和endswith_lower()
来完成,如果是c string的话,需要自己实现相关的接口,并且std::string
也并没有直接可以使用的API。
知乎上也有相关的讨论,见为什么大多数的C++的开源库都喜欢自己实现一个string?
n4700中关于std::basic_string_view的描述如下:
The class template
basic_string_view
describes an object that can refer to a constant contiguous sequence of char-like objects with the first element of the sequence at position zero.
关于std::basic_string_view<>
上面有三点需要注意:
char-like
type,[strings.general]
给出的描述也很含糊,如果谁能给出确切的定义还望告知。我在goldbolt试了一下,int什么的是没问题的。constant
的内容,这样描述的原因是由于std::basic_string_view
中存储的指向该内容的指针是const的contiguous
的序列std::basic_string_view
特化了以下四种类型的string_view:
Type | Definition |
---|---|
std::experimental::string_view | std::experimental::basic_string_view |
std::experimental::wstring_view | std::experimental::basic_string_view |
std::experimental::u16string_view | std::experimental::basic_string_view |
std::experimental::u32string_view | std::experimental::basic_string_view |
std::basic_string_view
的数据成员如下:
template>
class basic_string_view {
public:
//...
private:
const_pointer data_;
size_type size_;
};
注:由于是const_pointer,所以std::basic_string_view
的iterator
和const_iterator
本质上是一样的
从上面可以看到std::basic_string_view
对象非常简单,所占内存也比较小。但是提供了巨多的接口,除了涵盖std::string
的接口之外,还另外提供了很多API,例如下面三个API,更多的见C++ Standard。
constexpr void remove_prefix(size_type n);
1. Requires:n<= size
2. Effects: Equivalent todata_ += n; size -= n
constexpr void remove_suffix(size_type n)
1. Requires:n <= size()
2. Effects: Equivalent tosize -= n
constexpr void swap(basic_string_view& s) noexcept
1. Effects: Exchange the values of*this
ands
注意上面的remove_prefix
和remove_suffix
并不是真的修改原有的内容只是构造了一个新的std::string_view
。
关于std::string_view
比较好的入门资料见CppCon 2015: Marshall Clow “string_view”,string_view: a non-owning reference to a string, revision 4。
std::string_view
的优势std::string_view
的优势就是在处理字符串时,提供了比原先效率更高的解决方案,毕竟绝大部分情况下处理std::string_view
比直接处理std::string
要更高效。盗取CppCon 2015: Marshall Clow “string_view”中的例子如下:
string extract_part(const string &bar) {
return bar.substr(2, 3);
}
if (extract_part("ABCDEFG").front() == ''C) {
/* do something */
}
extract_part()
函数会有多次的字符串构造和析构的开销,而使用std::string_view
以后,开销就涉及到std::string_view
的拷贝开销,如下所示。
string_view extract_part(string_view bar) {
return bar.substr(2, 3);
}
if (extract_part("ABCDEFG").front() == 'C') {
/* do something */
}
std::string_view
中的substr
就只是简单的指针的加减,C++ Standard中也明确到std::string_view
的成员方法的复杂度都是O(1)。
关于std::string_view
的优势What is string_view?中有所提及,我摘取一部分。
The purpose of any and all kinds of “string reference” and “array reference” proposals is to avoid copying data which is already owned somewhere else and of which only a non-mutating view is required.
Such a view-handle class could be passed around cheaply by value and would offer cheap substringing operations (which can be implemented as simple pointer increments and size adjustments).
另外关于std::string_view
vs const std::string&
的讨论也有很多没例如How exactly std::string_view is faster than const std::string&?
,
我暂时能想到的std::string_view
所能带来的安全问题分为如下三种:
std::string_view
超出了其所描述数据的声明周期,例如将std::string_view
像普通的std::string
一样,从函数中返回或者是来回拷贝,这些操作都是危险的std::string_view
存在与std::string
的越界问题std::string_view
不需要null-terminated,有可能会引发相应的安全问题,但是我还没有找到合适的例子