std::initializer_list中的std::shared_ptr似乎会过早销毁

3

编辑:这确实是Visual Studio中的一个错误,并且已经被修复。在将更新2应用于Visual Studio之后,此问题无法再现(发布候选版本可在此处获得)。我很抱歉;我以为我的补丁已经更新到最新了。


我无法理解为什么在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;
}

我原本期望 initializer_list 能够拥有创建的 shared_ptr,并在 main 结束前保持它们的范围。

奇怪的是,如果我尝试访问列表中的第二个元素,则会得到预期的行为。例如:

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

考虑到这些因素,我猜测这可能是编译器的一个bug,但我想确保我没有忽略某些解释为什么会出现这种行为的原因(例如,也许在initializer_list中如何处理rvalues)。
这种行为是否可以在其他编译器中重现,或者有人能够解释可能发生的事情?

似乎在g++ 4.7上运行良好。可能需要补充分析,编译器可能存在问题。 - Benoît
2
请注意,可能正在销毁一个shared_ptrstd::shared_ptr<Base>对象是通过转换临时的std::shared_ptr<Derived1>(和2)对象来初始化的。 但这不应该导致Derived1(和2)对象本身的死亡,因为在shared_ptr转换期间,每个对象的引用计数应该达到2。或者也许移动构造函数用于转换,并且引用计数被窃取了。 - Ben Voigt
@Lilshieste:我刚刚追踪了一下代码执行。在初始化器列表构造的末尾,有两个 shared_ptr 对象被释放,一个是空的 shared_ptr<Derived1>(正确!),但另一个是一个 shared_ptr<Base>,它持有其中一个对象,然后将其释放。我已经确认通过移动构造函数来窃取引用,所以它实际上从未到达 2。 - Ben Voigt
@MooingDuck:是的,除了这个已经被修复了,据说这个还没有。 - Ben Voigt
@BenVoigt:修复已于2013年11月25日_check in_,将“在未来的Visual C++版本中发布”。 - Mooing Duck
显示剩余15条评论
2个回答

4

查看原始答案以分析问题代码中对象生命周期。这个回答仅隔离错误。


我制作了一个最小化的复制版本。虽然有更多代码,但涉及的库代码要少得多。并且更容易跟踪。

#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;
}

输出在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

以及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

这绝对是编译器的一个漏洞,且是一个相当严重的漏洞。如果你在Connect上提交了报告,我和其他人会乐意支持。


好主意,建立代码以最小化库的参与来重现问题。 - Lilshieste
@Lilshieste:它使得输入调试输出并查看(错乱)事件的确切顺序变得非常容易。 - Ben Voigt

1
make_shared返回的shared_ptr对象是临时的。在初始化shared_ptr<Base>实例后,它们将在完整表达式结束时被销毁。
但用户对象(Derived1Derived2)的所有权应该是共享的(或者如果你愿意的话,可以说是“转移的”),并且应该一直存在直到main结束。
我刚刚使用Visual Studio 2013运行了您问题中的代码,并没有出现访问冲突。奇怪的是,当我跟踪到main()~Base()时,输出如下:
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()

看起来有些不对。

如果我对GetValue()的返回值进行操作,结果是错误的(应该是1而不是0),并且会出现访问冲突。这种情况发生在所有跟踪输出之后,但似乎有些间歇性。

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()

这是我正在使用的代码的最终版本:

#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;
}

通过调试可以发现,在初始化列表构造完成后,对于类型为shared_ptr<Derived1>的对象(正确的,它已被移动到shared_ptr<Base>),析构函数会立即被调用,而对应的shared_ptr<Base>则非常错误。

谢谢您的帮助。我将继续创建一个Connect工单,以便从微软那里获得一些反馈。 - Lilshieste
@Lilshieste:请检查我的全新答案,它在没有std::shared_ptr的情况下重现了问题。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接