3

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 to Visual Studio (release candidate available here). I apologize; I thought I was up to date with my patches.


I can't for the life of me figure out why I get a seg fault when I run the following code in Visual Studio 2013:

#include <initializer_list>
#include <memory>

struct Base
{
    virtual int GetValue() { return 0; }
};

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);
    (*iter)->GetValue(); // access violation

    return 0;
}

I was expecting the initializer_list to take ownership of the created shared_ptrs, keeping them in scope until the end of main.

Oddly enough, if I try to access the second item in the list, I get the expected behavior. For example:

    auto iter = std::begin(foo) + 1;
    (*iter)->GetValue(); // returns 2

Considering these things, I'm guessing this may be a bug in the compiler - but I wanted to make sure I wasn't overlooking some explanation for why this behavior might be expected (e.g., maybe in how rvalues are handled in initializer_lists).

Is this behavior reproducible in other compilers, or can someone explain what might be happening?

Lilshieste
  • 2,744
  • 14
  • 20
  • Do you get the same failure if you instrument the destructors (with `std::cerr` or `OutputDebugString`, for example)? If the behavior persists, do the destructors run beforehand? – Ben Voigt Apr 07 '14 at 22:38
  • Seems to work fine with g++ 4.7. May add up to your analysis that there may be an issue with the compiler. – Benoît Apr 07 '14 at 22:40
  • 2
    Note that there probably IS a `shared_ptr` getting destroyed. The `std::shared_ptr` objects are initialized by converting temporary `std::shared_ptr` (and 2) objects. But that shouldn't cause death of the `Derived1` (and 2) objects themselves since the reference count for each should reach 2 during the `shared_ptr` conversion. Or perhaps move constructors are used for conversion and the reference count gets stolen. – Ben Voigt Apr 07 '14 at 22:40
  • Not that this helps much, but likewise works correctly (debugged and all) on clang++; (Apple LLVM version 5.1 (clang-503.0.38) (based on LLVM 3.4svn)). Likewise with [g++ 4.8](http://coliru.stacked-crooked.com/a/366884165044cd31). – WhozCraig Apr 07 '14 at 22:43
  • Thanks for the info re: other compilers, all. @BenVoigt That makes sense. I had tried using the debugger to step into some of the MSFT headers to get a better idea of what was happening, but quickly lost myself. As an additional data point: if I create 4 items in the list like this, the accessing the first *two* result in access violations. – Lilshieste Apr 07 '14 at 23:04
  • @Lilshieste: I just traced through the execution. At the end of the initializer list construction, two `shared_ptr` objects get freed, one is a `shared_ptr` which is empty (correct!), but the other is a `shared_ptr` holding one of the objects, which it frees. And I did confirm that the reference gets stolen via move-construction, so it never actually reaches 2. – Ben Voigt Apr 07 '14 at 23:07
  • @Lilshieste: if you make a connect ticket, place a link here – Mooing Duck Apr 07 '14 at 23:31
  • https://connect.microsoft.com/VisualStudio/feedback/details/809243/c-11-initializer-lists-as-default-argument seems to be it (and some related items https://connect.microsoft.com/VisualStudio/feedback/details/809243/c-11-initializer-lists-as-default-argument and http://connect.microsoft.com/VisualStudio/feedback/details/800104/) /cc @MooingDuck – sehe Apr 07 '14 at 23:40
  • I don't think that first or second one are related, those refer to VC++ selecting the wrong constructor, rather than destructing at the right times. That third one looks relevant though. – Mooing Duck Apr 07 '14 at 23:52
  • @MooingDuck: Yeah, except that one's fixed, supposedly, and this one isn't. – Ben Voigt Apr 08 '14 at 00:04
  • @BenVoigt: The fix was _checked in_ 11/25/2013, to be "in a future release of Visual C++". – Mooing Duck Apr 08 '14 at 00:17
  • @MooingDuck: We've had an Update released since then. BTW do you have the version string from the cl.exe is VS2013 handy? – Ben Voigt Apr 08 '14 at 00:19
  • This is embarassing, I forget to install said Update. Or something. Really confused by compiler patch versions right now. – Ben Voigt Apr 08 '14 at 00:25
  • @BenVoigt: To your credit, I didn't know there was an update since then, so I was uninformed. Let us know if the update has the patch – Mooing Duck Apr 08 '14 at 00:34
  • @sehe Thanks for calling this out. On my home machine, which is running Update 2 CTP 2, *I cannot reproduce this issue*. I thought my work machine was running the latest update, but it very well may not be. I'll post back here in the morning when I can check things out on my workstation. Apologies for the confusion so far, everyone. – Lilshieste Apr 08 '14 at 03:34
  • BTW, Update 2 has moved from CTP to Release Candidate status. http://www.microsoft.com/en-us/download/confirmation.aspx?id=42307 – Ben Voigt Apr 08 '14 at 04:32
  • This was keeping me up, so I remotely applied the update to my work machine. *The issue has been addressed by Update 2, at the latest.* I've edited the question to include a link to the Update 2 RC. – Lilshieste Apr 08 '14 at 04:33
  • @Lilshieste: No worries, I spent a good deal of time searching my email for the latest Update prerelease. When all along the latest was a public build, not a private invite. Arggg – Ben Voigt Apr 08 '14 at 04:34
  • @BenVoigt Yeah, I hadn't realized that until I went to update my workstation. (Oh well, even better!) Anyway, thanks again for all the diagnostic help. – Lilshieste Apr 08 '14 at 04:37
  • @Lilshieste: No problem, this stuff is fun – Ben Voigt Apr 08 '14 at 04:40

2 Answers2

4

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.

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
1

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.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Thanks for the help. I'm going to go ahead and create a Connect ticket to get some feedback from Microsoft on this. – Lilshieste Apr 07 '14 at 23:21
  • @Lilshieste: Please check my brand-new answer which reproduces the problem with no `std::shared_ptr`. – Ben Voigt Apr 07 '14 at 23:23