今天写算法是用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对象