Pimpl + QSharedPointer — Destructor = Disaster

Вчера я столкнулся с несчастьем, которое заняло у меня 24 часа разочарования. Проблема сводилась к неожиданным сбоям, происходящим на случайный основа. Чтобы усложнить ситуацию, отчеты об отладке имели абсолютно случайный шаблон также. Чтобы усложнить это еще больше, все следы отладки привели к случайный Исходники Qt или собственные библиотеки DLL, то есть каждый раз доказывающие, что проблема скорее не в моей стороне.

Вот несколько примеров таких прекрасных докладов:

Program received signal SIGSEGV, Segmentation fault.
0x0000000077864324 in ntdll!RtlAppendStringToString () from C:\Windows\system32\ntdll.dll
(gdb) bt
#0 0x0000000077864324 in ntdll!RtlAppendStringToString () from C:\Windows\system32\ntdll.dll
#1 0x000000002efc0230 in ?? ()
#2 0x0000000002070005 in ?? ()
#3 0x000000002efc0000 in ?? ()
#4 0x000000007787969f in ntdll!RtlIsValidHandle () from C:\Windows\system32\ntdll.dll
#5 0x0000000000000000 in ?? ()

warning: HEAP: Free Heap block 307e5950 modified at 307e59c0 after it was freed
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00000000778bf0b2 in ntdll!ExpInterlockedPopEntrySListFault16 () from C:\Windows\system32\ntdll.dll
(gdb) bt
#0 0x00000000778bf0b2 in ntdll!ExpInterlockedPopEntrySListFault16 () from C:\Windows\system32\ntdll.dll
#1 0x000000007786fd34 in ntdll!RtlIsValidHandle () from C:\Windows\system32\ntdll.dll
#2 0x0000000077910d20 in ntdll!RtlGetLastNtStatus () from C:\Windows\system32\ntdll.dll
#3 0x00000000307e5950 in ?? ()
#4 0x00000000307e59c0 in ?? ()
#5 0x00000000ffffffff in ?? ()
#6 0x0000000000220f10 in ?? ()
#7 0x0000000077712d60 in WaitForMultipleObjectsEx () from C:\Windows\system32\kernel32.dll
#8 0x0000000000000000 in ?? ()

Program received signal SIGSEGV, Segmentation fault.
0x0000000000a9678a in QBasicAtomicInt::ref (this=0x8) at ../../include/QtCore/../../../qt-src/src/corelib/arch/qatomic_x86_64.h:121
121 : "memory");
(gdb) bt
#0 0x0000000000a9678a in QBasicAtomicInt::ref (this=0x8) at ../../include/QtCore/../../../qt-src/src/corelib/arch/qatomic_x86_64.h:121
#1 0x00000000009df08e in QVariant::QVariant (this=0x21e4d0, p=...) at d:/Distributions/qt-src/src/corelib/kernel/qvariant.cpp:1426
#2 0x0000000000b4dde9 in QList<QVariant>::value (this=0x323bd480, i=1) at ../../include/QtCore/../../../qt-src/src/corelib/tools/qlist.h:666
#3 0x00000000009ccff7 in QObject::property (this=0x3067e900,
name=0xa9d042a <QCDEStyle::drawPrimitive(QStyle::PrimitiveElement, QStyleOption const*, QPainter*, QWidget const*) const::pts5+650> "_q_stylerect")
at d:/Distributions/qt-src/src/corelib/kernel/qobject.cpp:3742
#4 0x0000000000000000 in ?? ()

Как видите, этот материал довольно неприятен, он не дает никакой полезной информации. Но была одна вещь, на которую я не обращал внимания. Это было странное предупреждение во время компиляции, которое также трудно уловить глазом:

In file included from d:/Libraries/x64/MinGW-w64/4.7.2/Qt/4.8.4/include/QtCore/qsharedpointer.h:50:0,
from d:/Libraries/x64/MinGW-w64/4.7.2/Qt/4.8.4/include/QtCore/QSharedPointer:1,
from ../../../../source/libraries/Project/sources/Method.hpp:4,
from ../../../../source/libraries/Project/sources/Slot.hpp:4,
from ../../../../source/libraries/Project/sources/Slot.cpp:1:
d:/Libraries/x64/MinGW-w64/4.7.2/Qt/4.8.4/include/QtCore/qsharedpointer_impl.h: In instantiation of 'static void QtSharedPointer::ExternalRefCount<T>::deref(QtSharedPointer::ExternalRefCount<T>::Data*, T*) [with T = Project::Method::Private; QtSharedPointer::ExternalRefCount<T>::Data = QtSharedPointer::ExternalRefCountData]':
d:/Libraries/x64/MinGW-w64/4.7.2/Qt/4.8.4/include/QtCore/qsharedpointer_impl.h:336:11:   required from 'void QtSharedPointer::ExternalRefCount<T>::deref() [with T = Project::Method::Private]'
d:/Libraries/x64/MinGW-w64/4.7.2/Qt/4.8.4/include/QtCore/qsharedpointer_impl.h:401:38:   required from 'QtSharedPointer::ExternalRefCount<T>::~ExternalRefCount() [with T = Project::Method::Private]'
d:/Libraries/x64/MinGW-w64/4.7.2/Qt/4.8.4/include/QtCore/qsharedpointer_impl.h:466:7:   required from here
d:/Libraries/x64/MinGW-w64/4.7.2/Qt/4.8.4/include/QtCore/qsharedpointer_impl.h:342:21: warning: possible problem detected in invocation of delete operator: [enabled by default]
d:/Libraries/x64/MinGW-w64/4.7.2/Qt/4.8.4/include/QtCore/qsharedpointer_impl.h:337:28: warning: 'value' has incomplete type [enabled by default]

На самом деле, я обратился к этому предупреждению только в качестве крайней меры, потому что в таком отчаянном стремлении найти ошибку, код уже был заражен буквально до смерти.

Внимательно прочитав его, я вспомнил, что, например, если std::unique_ptr или же std::scoped_ptr за Pimpl — конечно, нужно предоставить десктруктор, иначе код даже не скомпилируется. Тем не менее, я также помню, что std::shared_ptr не заботится о деструкторе и прекрасно работает без него. Это была еще одна причина, по которой я не обратил внимания на это странное предупреждение. Короче говоря, когда я добавил деструктор, этот случайный сбой прекратился. Похоже, Qt’s QSharedPointer имеет некоторые недостатки дизайна по сравнению с std::shared_ptr, Я думаю, было бы лучше, если бы разработчики Qt преобразовали это предупреждение в ошибка потому что такие отладочные марафоны просто не стоят своего времени, усилий и нервы.

Мои вопросы:

  1. Что случилось с QSharedPointer? Почему деструктор так важен?
  2. Почему сбой произошел, когда не было деструктора? Эти объекты (которые используют Pimpl + QSharedPointer) создаются в стеке, и никакие другие объекты не имеют к ним доступа после их смерти. Тем не менее, сбой произошел во время некоторых случайный период времени после их смерти.
  3. Кто-нибудь сталкивался с такими проблемами раньше? Пожалуйста, поделитесь своим опытом.
  4. Есть ли другие подводные камни?
    как в Qt — те, которые я должен знать наверняка, чтобы остаться
    безопасно в будущем?

Надеюсь, эти вопросы и мой пост в целом помогут другим избежать ада, в котором я был последние 24 часа.

8

Решение

Проблема была решена в Qt 5, смотрите https://codereview.qt-project.org/#change,26974

Компилятор, вызывающий неправильный деструктор или предполагающий другую структуру памяти, вероятно, приведет к некоторому повреждению памяти. Я бы сказал, что компилятор должен выдавать ошибку для этой проблемы, а не предупреждение.

3

Другие решения

Вы столкнетесь с аналогичной проблемой с std::unique_ptr, что также может привести к поломке деструкторов, если используется с неполным типом. Исправление, конечно, довольно тривиально — я объявляю конструктор для класса, а затем определяю его в файле реализации как

MyClass::~MyClass() = default;

Причина того, что это проблема для std::unique_ptr но нет std::shared_ptr является то, что деструктор является частью типа первого, но является членом второго.

1

По вопросам рекламы [email protected]