std::shared_ptr in an std::initializer_list appears to be getting destroyed prematurely

后端 未结 2 922
时光取名叫无心
时光取名叫无心 2020-12-18 12:46

Edit: This is indeed caused by a bug in Visual Studio - and it has already been fixed. The issue is not reproducible after applying Update 2

相关标签:
2条回答
  • 2020-12-18 13:03

    The shared_ptr objects returned from make_shared are temporaries. They will be destroyed at the end of the full-expression, after being used to initialize shared_ptr<Base> instances.

    But ownership of the user objects (the Derived1 and Derived2) should be shared (or "transferred" if you like) to the shared_ptr instances in the list. Those user objects should live until the end of main.

    I just ran the code from your question using Visual Studio 2013 and got no access violation. Oddly, when I trace to main() and ~Base(), I get the following output:

    C:\Code\SO22924358>cl /EHsc main.cpp
    Microsoft (R) C/C++ Optimizing Compiler Version 18.00.21005.1 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    main.cpp
    Microsoft (R) Incremental Linker Version 12.00.21005.1
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /out:main.exe
    main.obj
    
    C:\Code\SO22924358>main
    ~Base()
    Reached end of main
    ~Base()
    

    That does look wrong.

    And if I do something with the return value of GetValue(), it is wrong (0 instead of 1) and I get the access violation. It occurs after all tracing output, however. And it seems somewhat intermittent.

    C:\Code\SO22924358>cl /Zi /EHsc main.cpp
    Microsoft (R) C/C++ Optimizing Compiler Version 18.00.21005.1 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    main.cpp
    Microsoft (R) Incremental Linker Version 12.00.21005.1
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /out:main.exe
    /debug
    main.obj
    
    C:\Code\SO22924358>main
    ~Base()
    GetValue() returns 0
    Reached end of main
    ~Base()
    

    Here's the final version of the code I'm working with:

    #include <initializer_list>
    #include <memory>
    #include <iostream>
    
    struct Base
    {
        virtual int GetValue() { return 0; }
        ~Base() { std::cerr << "~Base()" << std::endl; }
    };
    
    struct Derived1 : public Base
    {
        int GetValue() override { return 1; }
    };
    
    struct Derived2 : public Base
    {
        int GetValue() override { return 2; }
    };
    
    int main()
    {
        std::initializer_list< std::shared_ptr<Base> > foo
            {
                std::make_shared<Derived1>(),
                std::make_shared<Derived2>()
            };
    
        auto iter = std::begin(foo);
        std::cerr << "GetValue() returns " << (*iter)->GetValue() << std::endl; // access violation
    
        std::cerr << "Reached end of main" << std::endl;
    
        return 0;
    }
    

    Stepping through shows that destructors are called immediately after initializer list construction for shared_ptr<Derived1> (correct, its object has been moved to a shared_ptr<Base>), and the matching shared_ptr<Base>, which is very very wrong.

    0 讨论(0)
  • 2020-12-18 13:06

    See the original answer for analysis of object lifetimes of the code in the question. This one isolates the bug.


    I made a minimal reproduction. It's more code, but a lot less library code involved. And easier to trace.

    #include <initializer_list>
    
    template<size_t N>
    struct X
    {
        int i = N;
    
        typedef X<N> self;
        virtual int GetValue() { return 0; }
        X()                               { std::cerr << "X<" << N << ">() default ctor" << std::endl; }
        X(const self& right) : i(right.i) { std::cerr << "X<" << N << ">(const X<" << N << "> &) copy-ctor" << std::endl; }
        X(self&& right)      : i(right.i) { std::cerr << "X<" << N << ">(X<" << N << ">&&      ) moving copy-ctor" << std::endl; }
    
        template<size_t M>
        X(const X<M>& right) : i(right.i) { std::cerr << "X<" << N << ">(const X<" << M << "> &) conversion-ctor" << std::endl; }
        template<size_t M>
        X(X<M>&& right)      : i(right.i) { std::cerr << "X<" << N << ">(X<" << M << ">&&      ) moving conversion-ctor" << std::endl; }
    
        ~X() { std::cerr << "~X<" << N << ">(), i = " << i << std::endl; }
    };
    
    template<size_t N>
    X<N> make_X() { return X<N>{}; }
    
    #include <iostream>
    int main()
    {
        std::initializer_list< X<0> > foo
            {
                make_X<1>(),
                make_X<2>(),
                make_X<3>(),
                make_X<4>(),
            };
    
        std::cerr << "Reached end of main" << std::endl;
    
        return 0;
    }
    

    The output is BAD on both x64:

    C:\Code\SO22924358>cl /EHsc minimal.cpp
    Microsoft (R) C/C++ Optimizing Compiler Version 18.00.21005.1 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    minimal.cpp
    Microsoft (R) Incremental Linker Version 12.00.21005.1
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /out:minimal.exe
    minimal.obj
    
    C:\Code\SO22924358>minimal
    X<1>() default ctor
    X<0>(X<1>&&      ) moving conversion-ctor
    X<2>() default ctor
    X<0>(X<2>&&      ) moving conversion-ctor
    X<3>() default ctor
    X<0>(X<3>&&      ) moving conversion-ctor
    X<4>() default ctor
    X<0>(X<4>&&      ) moving conversion-ctor
    ~X<0>(), i = 2
    ~X<2>(), i = 2
    ~X<0>(), i = 1
    ~X<1>(), i = 1
    Reached end of main
    ~X<0>(), i = 4
    ~X<0>(), i = 3
    ~X<0>(), i = 2
    ~X<0>(), i = 1
    

    and x86:

    C:\Code\SO22924358>cl /EHsc minimal.cpp
    Microsoft (R) C/C++ Optimizing Compiler Version 18.00.21005.1 for x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    minimal.cpp
    Microsoft (R) Incremental Linker Version 12.00.21005.1
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /out:minimal.exe
    minimal.obj
    
    C:\Code\SO22924358>minimal
    X<1>() default ctor
    X<0>(X<1>&&      ) moving conversion-ctor
    X<2>() default ctor
    X<0>(X<2>&&      ) moving conversion-ctor
    X<3>() default ctor
    X<0>(X<3>&&      ) moving conversion-ctor
    X<4>() default ctor
    X<0>(X<4>&&      ) moving conversion-ctor
    ~X<0>(), i = 2
    ~X<2>(), i = 2
    ~X<0>(), i = 1
    ~X<1>(), i = 1
    Reached end of main
    ~X<0>(), i = 4
    ~X<0>(), i = 3
    ~X<0>(), i = 2
    ~X<0>(), i = 1
    

    Definitely a compiler bug, and a pretty severe one. If you file a report on Connect I and many others will be happy to upvote.

    0 讨论(0)
提交回复
热议问题