今天写算法是用initializer_list时出现问题了,调了很久才调好,特地的去了解下initializer_list这个类
一、问题重现
考虑如下代码:
#include "iostream"
#include "initializer_list"
#include "algorithm"
#define P(r) for_each((r).begin(), (r).end(), [](auto & t) {cout << t << "\t";})
using namespace std;
initializer_list<int> f()
{
return { 1,2,3,4 };
}
int main()
{
auto r = f();
P(r);
cout << endl;
system("pause");
return 0;
}
f函数返回一个initializer_list<int>对象,里面有4个元素1,2,3,4,并在main函数里输出,乍一看好像并没有什么毛病,可是运行结果却出人意料:
-858993460 -858993460 -858993460 -858993460
请按任意键继续. . .
上面结果是在VS2017中Debug运行产生的(注意一定是Debug,Release会优化代码),为什么会这样呢?经过思考我决定将initializer_list改成vector:
#include "iostream"
#include "initializer_list"
#include "algorithm"
#include "vector"
#define P(r) for_each((r).begin(), (r).end(), [](auto & t) {cout << t << "\t";})
using namespace std;
vector<int> f()
{
return { 1,2,3,4 };
}
int main()
{
auto r = f();
P(r);
cout << endl;
system("pause");
return 0;
}
同样的环境下运行,得到如下结果:
1 2 3 4
请按任意键继续. . .
二、分析问题
为什么会这样呢?我们首先能考虑到的是initializer_list对象本身没有存放1,2,3,4这四个元素,而vector对象本身存放了1,2,3,4,查看这两个类的源代码:
template<class _Ty,
class _Alloc = allocator<_Ty>>
class vector
: public _Vector_alloc<_Vec_base_types<_Ty, _Alloc>>
{ // varying size array of values
private:
using _Mybase = _Vector_alloc<_Vec_base_types<_Ty, _Alloc>>;
using _Alty = typename _Mybase::_Alty;
using _Alty_traits = typename _Mybase::_Alty_traits;
template<class _Elem>
class initializer_list
{ // list of pointers to elements
public:
typedef _Elem value_type;
typedef const _Elem& reference;
typedef const _Elem& const_reference;
typedef size_t size_type;
typedef const _Elem* iterator;
typedef const _Elem* const_iterator;
......
private:
const _Elem *_First;
const _Elem *_Last;
注意到vector对象维护一个_MyBase对象(这个对象存放上面的1,2,3,4),而initializer_list这仅仅只有_First和_Last两个指针,看来我们想法是正确的:vector是值语义的,而initializer_list是指针语义的.
进一步的,我们想这道initializer_list<int> x = {1,2,3,4};是如何构造出来的,编译器究竟帮我们做了些什么事,1,2,3,4这四个元素究竟存放在哪里,为什么使用initializer_list得不到正确答案?带着这一系列的问题,我们对如下函数进行反汇编(实际上,我在这一问题上查了很多资料,都没有得到自己想要的结果,最终突然想到反汇编):
initializer_list<int> f()
{
return { 1,2,3,4 };
}
汇编代码如下:
initializer_list<int> f()
{
00DB2AE0 push ebp
00DB2AE1 mov ebp,esp
00DB2AE3 sub esp,0D8h
00DB2AE9 push ebx
00DB2AEA push esi
00DB2AEB push edi
00DB2AEC lea edi,[ebp-0D8h]
00DB2AF2 mov ecx,36h
00DB2AF7 mov eax,0CCCCCCCCh
00DB2AFC rep stos dword ptr es:[edi]
00DB2AFE mov ecx,offset _A9247D34_initializer_listtest@cpp (0DCB028h)
00DB2B03 call @__CheckForDebuggerJustMyCode@4 (0DB1519h)
return { 1,2,3,4 };
00DB2B08 mov dword ptr [ebp-0D4h],1
00DB2B12 mov dword ptr [ebp-0D0h],2
00DB2B1C mov dword ptr [ebp-0CCh],3
00DB2B26 mov dword ptr [ebp-0C8h],4
00DB2B30 lea eax,[ebp-0C4h]
00DB2B36 push eax
00DB2B37 lea ecx,[ebp-0D4h]
00DB2B3D push ecx
return { 1,2,3,4 };
00DB2B3E mov ecx,dword ptr [ebp+8]
00DB2B41 call std::initializer_list<int>::initializer_list<int> (0DB1573h)
00DB2B46 mov eax,dword ptr [ebp+8]
}
00DB2B49 pop edi
00DB2B4A pop esi
00DB2B4B pop ebx
00DB2B4C add esp,0D8h
00DB2B52 cmp ebp,esp
00DB2B54 call __RTC_CheckEsp (0DB1546h)
00DB2B59 mov esp,ebp
00DB2B5B pop ebp
00DB2B5C ret
现在分析这段代码:
- 首先注意到sub esp,0D8h,这行语句先在栈上申请了D8H个字节的空间
- 然后一直执行到00DB2B03,此时栈的结构如下:
- 接下来的4行语句将1,2,3,4放入栈中:
- 接下来的4行语句为initializer_list的构造函数传入参数:
- 注意到initializer_list的构造函数:
constexpr initializer_list(const _Elem *_First_arg,
const _Elem *_Last_arg) noexcept
: _First(_First_arg), _Last(_Last_arg)
{ // construct with pointers
}
- 图中的begin代表_First_arg,end代表_Last_args
- 剩下的就是构造函数构造出这个initializer_list对象,不过这个对仅仅维护了这两个指针(指向栈地址)
- 注意到add esp,0D8h 将先前申请的空间释放了,所以此时initialzer_list对象的两个指针指向了错误的位置.
实际上,initializer_list的API已经提到了这一点:
The underlying array is not guaranteed to exist after the lifetime of the original initializer list object has ended. The storage for initializer_list is unspecified (i.e. it could be automatic, temporary, or static read-only memory, depending on the situation). |
大费周折后,我们终于知道为什么最开始的代码为什么有问题了,接下我们来看看使用时vector的反汇编代码:
vector<int> f()
{
00B95C03 sub esp,100h
00B95C09 push ebx
00B95C0A push esi
00B95C0B push edi
00B95C0C lea edi,[ebp-100h]
00B95C12 mov ecx,40h
00B95C17 mov eax,0CCCCCCCCh
00B95C1C rep stos dword ptr es:[edi]
00B95C1E mov dword ptr [ebp-0E4h],0
00B95C28 mov ecx,offset _A9247D34_initializer_listtest@cpp (0BAB028h)
00B95C2D call @__CheckForDebuggerJustMyCode@4 (0B91519h)
return { 1,2,3,4 };
00B95C32 mov dword ptr [ebp-0FCh],1
00B95C3C mov dword ptr [ebp-0F8h],2
00B95C46 mov dword ptr [ebp-0F4h],3
00B95C50 mov dword ptr [ebp-0F0h],4
00B95C5A lea ecx,[ebp-0C5h]
00B95C60 call std::allocator<int>::allocator<int> (0B91929h)
00B95C65 push eax
00B95C66 lea eax,[ebp-0ECh]
00B95C6C push eax
00B95C6D lea ecx,[ebp-0FCh]
00B95C73 push ecx
00B95C74 lea ecx,[ebp-0D8h]
00B95C7A call std::initializer_list<int>::initializer_list<int> (0B91573h)
00B95C7F mov edx,dword ptr [eax+4]
00B95C82 push edx
00B95C83 mov eax,dword ptr [eax]
00B95C85 push eax
00B95C86 mov ecx,dword ptr [ebp+8]
00B95C89 call std::vector<int,std::allocator<int> >::vector<int,std::allocator<int> > (0B919A6h)
00B95C8E mov ecx,dword ptr [ebp-0E4h]
00B95C94 or ecx,1
00B95C97 mov dword ptr [ebp-0E4h],ecx
00B95C9D mov eax,dword ptr [ebp+8]
}
00B95CA0 pop edi
00B95CA1 pop esi
00B95CA2 pop ebx
00B95CA3 add esp,100h
00B95CA9 cmp ebp,esp
00B95CAB call __RTC_CheckEsp (0B91546h)
00B95CB0 mov esp,ebp
00B95CB2 pop ebp
00B95CB3 ret
我们注意到 00B95C60 call std::allocator<int>::allocator<int> (0B91929h) 这行语句将1,2,3,4拷贝进了堆内存由vector维护
三、得出结论
- vector是值语义的,而initializer_list是指针语义的
- initializer_list只维护两个指针,没有在堆上申请内存
- 编译器遇到类似{1,2,3,4}这样的语句是,会将1,2,3,4连续放在栈中并传入开始元素的指针和结束(后一个位置)元素的指针构造initializer对象