【问题标题】:Why does QSharedPointer<T>::create call destructor of incomplete object?为什么 QSharedPointer<T>::create 调用不完整对象的析构函数?
【发布时间】:2016-03-21 08:49:51
【问题描述】:

我有以下代码示例:

#include <QCoreApplication>
#include <QSharedPointer>
#include <QDebug>

#include <memory>

class A
{
public:
    A()
    {
        throw 1;
    }
    ~A() { qDebug() << "A destr"; }
};

int main(int argc, char* argv[])
{
    QCoreApplication a(argc, argv);

    try
    {
        //auto m1 = std::make_shared<A>();
        auto m2 = QSharedPointer<A>::create();
    }
    catch (...)
    {
        qDebug() << "catch!";
    }

    return a.exec();
}

上述代码的输出是:

A destr
catch!

但如果我取消注释 std::make_shared 的行,输出如下:

catch!

那么为什么QSharedPointer::create 会调用不完整对象的析构函数呢?这是一个错误还是我遗漏了什么?

我用MSVC2013 + Qt 5.5.1MSVC2015 + Qt 5.6 (从源代码构建)进行了尝试。结果是一样的。

【问题讨论】:

  • 作为参考,您的特定版本的 QSharedPointer::create() 的源代码在这里:code.qt.io/cgit/qt/qtbase.git/tree/src/corelib/tools/… - 我假设您在构建时没有定义 QT_SHAREDPOINTER_TRACK_POINTERS
  • 这是五年前提交的一个可能与 Qt 相关的错误(此后没有活动):bugreports.qt.io/browse/QTBUG-14637
  • @JohnZwinck 是的,它没有定义。据我了解,此标志用于调试拥有同一对象的多个智能指针。还是我应该试一试?
  • 无需定义它——我只是想了解您实际使用的代码路径。现在很明显,Qt 中存在一个错误,我在下面的回答中对此进行了描述。
  • @JohnZwinck 谢谢,评论前没有更新页面。

标签: c++ qt c++11 qsharedpointer


【解决方案1】:

最后,我们将拥有它fixed!我猜它会是 Qt 5.8.2 或 Qt 5.9。

感谢@JohnZwinck,您的想法很好。

【讨论】:

    【解决方案2】:

    看来您在 Qt 中发现了一个错误。我建议你提交一个错误报告并参考这个有点相关的错误:https://bugreports.qt.io/browse/QTBUG-14637

    问题似乎出在http://code.qt.io/cgit/qt/qtbase.git/tree/src/corelib/tools/qsharedpointer_impl.h?h=v5.5.1#n420 - 其简化代码如下所示:

    static inline QSharedPointer create()
    {
        typedef QtSharedPointer::ExternalRefCountWithContiguousData<T> Private;
        typename Private::DestroyerFn destroy = &Private::deleter;
    
        QSharedPointer result(Qt::Uninitialized);
        result.d = Private::create(&result.value, destroy);
    
        new (result.data()) T();
        result.d->setQObjectShared(result.value, true);
        result.enableSharedFromThis(result.data());
        return result;
    }
    

    对其他函数的引用有点复杂(大部分在同一个文件中),但似乎deleter 在通过放置new 调用构造函数之前存储在result 中。当您的构造函数抛出时,您的对象永远不会完全构造,但 QSharedPointer result 已经构造,并且包含删除器。从那里到deleter 函数有很短的距离:

    static void deleter(ExternalRefCountData *self)
    {
        ExternalRefCountWithContiguousData *that =
                static_cast<ExternalRefCountWithContiguousData *>(self);
        that->data.~T();
    }
    

    现在你的析构函数被调用了,尽管你的构造函数从未完成。那是未定义的行为。如果你不走运,这将破坏你的应用程序状态(因为它违反了只有在构造函数运行完成时才调用析构函数的规则——某些类类型可能依赖的规则)。

    一个可能的解决方法(我没有测试过,但你可以)是:

    static void noOpDeleter(ExternalRefCountData *self)
    {
        Q_UNUSED(self);
    }
    
    static inline QSharedPointer create()
    {
        typedef QtSharedPointer::ExternalRefCountWithContiguousData<T> Private;
        typename Private::DestroyerFn noDestroy = &noOpDeleter;
        typename Private::DestroyerFn destroy = &Private::deleter;
    
        QSharedPointer result(Qt::Uninitialized);
        result.d = Private::create(&result.value, noDestroy);
    
        new (result.data()) T();
        result.d->destroyer = destroy;
        result.d->setQObjectShared(result.value, true);
        result.enableSharedFromThis(result.data());
        return result;
    }
    

    如果您可以验证上述内容,您应该随时将其编入补丁并将其提交给 Qt 错误跟踪器。希望附上工作补丁后,他们会及时接受。

    【讨论】:

    • 这并不需要运气:调用不活动对象的析构函数是未定义的行为。对象的生命周期在其构造函数成功完成后开始。在许多情况下,UB 可能是无害的,但无论如何它都是 UB。
    • @Yakk:谢谢你,我已经更新了我的答案以反映你的观点。
    猜你喜欢
    • 2010-10-16
    • 1970-01-01
    • 2014-06-13
    • 2014-08-26
    • 2018-05-07
    • 2020-06-05
    • 2015-08-21
    • 1970-01-01
    相关资源
    最近更新 更多