Strange things happen: for almost a year, PVS-Studio has a plugin for Qt Creator. However, we haven't yet published a good old article with the IDE check. We've made amends and invite you to see how the recently revamped development environment lives.
Although Qt Creator is a pretty well-known IDE, it's not so well-known to skip over its brief description. Qt Creator is a cross-platform development environment for C++ applications that use the Qt framework. It provides a user-friendly interface for writing, debugging, and deploying apps. Also, Qt Creator has tools for working with the code editor, version control system, and other features. Qt Creator supports different operating systems: Windows, macOS, and Linux. Given that Qt has recently bought a company-developer of a static analyzer and integrated the tool into its IDE, it's very interesting to take a look at Qt Creator and see how good its internal analysis is :)
Why did I point out a "revamp" in the article annotation? It's simple: starting with the 5 version, developers drastically redesigned the IDE to meet modern standards. As a developer, I enjoy these changes. In many cases, Qt Creator is no worse, and in some cases, it's even better than Visual Studio and VS Code (it's my point of view; if you want to talk about it, you're welcome to comments). Of course, there is something we can scold the developers for (such us the API support for third-party developers) but perhaps not today and not here.
Have I already mentioned that there is a PVS-Studio extension for Qt Creator, right? That's why, I have used an asterisk with Qt Creator in the title. So, today, we're going to check the code of Qt Creator in Qt Creator using the PVS-Studio plugin for Qt Creator. What "ingredients" do we need? The recipe is pretty simple:
If you follow the recipe, you'll "cook" the following:
Let's start with the most absorbing things—memory leaks. To those who's off topic, I'll briefly explain the issue's crux. Qt has a non-trivial object ownership system, and to understand that there is no memory leak, you need to keep in mind that there are many ways to both create objects and transfer ownership (hello, setParent and layout's). I'll be honest: there have been plenty of false positives. However, I've found some interesting cases among the warnings. I suggest you take a look at them while we're enhancing our analyzer to reduce false positives. :)
Let's start with a small memory leak in the function for getting paths to the IDE crash reports.
The file: main.cpp:1813
QString crashReportsPath()
{
std::unique_ptr<Utils::QtcSettings> settings(createUserSettings());
QFileInfo(settings->fileName()).path() + "/crashpad_reports";
if (Utils::HostOsInfo::isMacHost())
return QFileInfo(createUserSettings()->fileName()).path() +
"/crashpad_reports";
else
return QCoreApplication::applicationDirPath() + '/' +
RELATIVE_LIBEXEC_PATH + "crashpad_reports";
}
If the reuse of the createUserSettings function confused you, congrats — you have a practiced eye for finding bugs. You're headed in the right direction. For the first time, the pointer obtained from the function is saved to std::unique_ptr and is correctly deleted when leaving the scope. For the second time, in the macOS branch, the pointer is forgotten after getting the path to the file. The analyzer report shows that:
V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The return value of the 'createUserSettings' function is not saved. A memory leak is possible. main.cpp 422
You may ask me, "Are you sure there is trouble?" Yes, I'm sure. The createUserSettings implementation looks like this:
The file: main.cpp:274
static Utils::QtcSettings *createUserSettings()
{
return new Utils::QtcSettings(
QSettings::IniFormat,
QSettings::UserScope,
QLatin1String(Core::Constants::IDE_SETTINGSVARIANT_STR),
QLatin1String(Core::Constants::IDE_CASED_ID));
}
I also found two cases where objects aren't assigned the parent objects, which are responsible for their deletion, while developers just forgot to delete them. The first case is quite simple: developers create an object but forget to delete it here, although everything is fine in other places.
The file: debuggerplugin.cpp:1813
void DebuggerPlugin::attachToProcess(....)
{
....
auto kitChooser = new KitChooser;
kitChooser->setShowIcons(true);
kitChooser->populate();
Kit *kit = kitChooser->currentKit();
dd->attachToRunningProcess(kit, processInfo, false);
}
In the second case, a model is created in the class constructor without parent. It would be better that only the model is deleted in the destructor, but alas, its implementation is empty. Developers might want to use any smart pointer but forgot about it...
The file: curveeditorview.cpp:43
CurveEditorView::CurveEditorView(
ExternalDependenciesInterface &externalDepoendencies
)
: AbstractView{externalDepoendencies}
, m_block(false)
, m_model(new CurveEditorModel())
, m_editor(new CurveEditor(m_model))
{
....
}
CurveEditorView::~CurveEditorView() {}
Here are the corresponding warnings:
"Next, I suggest you look at a classic of the genre", I'd say, and then show the dereferencing of non-checked pointers, however, there is one thing. We have a project that actively uses C++17 and std::optional, and that's great. But, unfortunately, the errors are called classical because they don't depend on the tools used. Will you try to find these errors by yourself? ;-)
The file: aspects.cpp:2189
void IntegerAspect::addToLayout(Layouting::LayoutItem &parent)
{
QTC_CHECK(!d->m_spinBox);
d->m_spinBox = createSubWidget<QSpinBox>();
d->m_spinBox->setDisplayIntegerBase(d->m_displayIntegerBase);
d->m_spinBox->setPrefix(d->m_prefix);
d->m_spinBox->setSuffix(d->m_suffix);
d->m_spinBox->setSingleStep(d->m_singleStep);
d->m_spinBox->setSpecialValueText(d->m_specialValueText);
if (d->m_maximumValue && d->m_maximumValue)
d->m_spinBox->setRange(
int(d->m_minimumValue.value() / d->m_displayScaleFactor),
int(d->m_maximumValue.value() / d->m_displayScaleFactor)
);
// Must happen after setRange()
d->m_spinBox->setValue(int(value() / d->m_displayScaleFactor));
addLabeledItem(parent, d->m_spinBox);
connect(d->m_spinBox.data(), &QSpinBox::valueChanged,
this, &IntegerAspect::handleGuiChanged);
}
If you found the m_maximumValue reuse instead of m_minimumValue, congrats! It wasn't that easy, was it? I think the Qt Creator developers would agree too, because I found the same code a little further down in the same file:
The file: aspects.cpp:2291
void DoubleAspect::addToLayout(LayoutItem &builder)
{
QTC_CHECK(!d->m_spinBox);
d->m_spinBox = createSubWidget<QDoubleSpinBox>();
d->m_spinBox->setPrefix(d->m_prefix);
d->m_spinBox->setSuffix(d->m_suffix);
d->m_spinBox->setSingleStep(d->m_singleStep);
d->m_spinBox->setSpecialValueText(d->m_specialValueText);
if (d->m_maximumValue && d->m_maximumValue)
d->m_spinBox->setRange(d->m_minimumValue.value(),
d->m_maximumValue.value());
bufferToGui(); // Must happen after setRange()!
addLabeledItem(builder, d->m_spinBox);
connect(d->m_spinBox.data(), &QDoubleSpinBox::valueChanged,
this, &DoubleAspect::handleGuiChanged);
}
By the way, do you often pay attention to what IntelliSense (or its analog) recommends you use? I think that's what caused the error. Before, I often caught myself writing the name of a variable, promptly hitting Enter, and moving on — there was no "Stop" sign for my thought. If it sounds familiar, share your experience in the comments. So we can see how many of us do it too. :)
Oh, of course, here are the warnings:
I suggest you do the eye exercise again and search for an error in the function for filtering compiler flags. Well, I can't let you go without a clue: "noitcnuf erapmoc eht ni setacilpud rof kooL". Yeah, it's not that easy.
The file: gcctoolchain.cpp:474
// For querying operations such as -dM
static QStringList filteredFlags(const QStringList &allFlags,
bool considerSysroot)
{
QStringList filtered;
for (int i = 0; i < allFlags.size(); ++i)
{
const QString &a = allFlags.at(i);
if (a.startsWith("--gcc-toolchain="))
{
filtered << a;
}
else if (a == "-arch")
{
if (++i < allFlags.length() && !filtered.contains(a))
filtered << a << allFlags.at(i);
}
else if (a == "-Xclang")
{
filtered << a;
}
else if ( (considerSysroot && (a == "--sysroot" || a == "-isysroot"))
|| a == "-D" || a == "-U"
|| a == "-gcc-toolchain"
|| a == "-target"
|| a == "-mllvm"
|| a == "-isystem")
{
if (++i < allFlags.length())
filtered << a << allFlags.at(i);
}
else if ( a.startsWith("-m") || a.startsWith("-f") || a.startsWith("-O")
|| a.startsWith("-std=") || a.startsWith("-stdlib=")
|| a.startsWith("-specs=") || a == "-ansi" || a == "-undef"
|| a.startsWith("-D") || a.startsWith("-U")
|| a.startsWith("-stdlib=") || a.startsWith("-B")
|| a.startsWith("--target=")
|| (a.startsWith("-isystem") && a.length() > 8)
|| a == "-Wno-deprecated"
|| a == "-nostdinc" || a == "-nostdinc++")
{
filtered << a;
}
}
return filtered;
}
If you can't immediately find an unnecessary check for the "-stdlib" substring, don't be upset because static analyzers were created to help us here. If you can, then congratulations! You have a keen eye.
Alas, the gifts are given at the end of the article, so I've asked an AI to generate a medal for the true detective programmer:
I agree, it a bit lacks a programmer's spirit. However, it turned out pretty good. Oh, and I almost forgot about the analyzer warning:
V501 [CWE-570] There are identical sub-expressions 'a.startsWith("-stdlib=")' to the left and to the right of the '||' operator. gcctoolchain.cpp 491.
Honestly, I really wanted to paste the full code of this function here (which has almost 400 code lines) to make looking for errors more interesting. But I don't want to abuse your mouse wheel, so I brought only curious part.
The file: symbolgroupvalue.cpp:1196
static KnownType knownClassTypeHelper(const std::string &type,
std::string::size_type pos,
std::string::size_type endPos)
{
....
// Remaining non-template types
switch (endPos - qPos)
{
....
case 30:
if (!type.compare(qPos, 30, "QPatternist::SequenceType::Ptr"))
return KT_QPatternist_SequenceType_Ptr;
if (!type.compare(qPos, 30, "QXmlStreamNamespaceDeclaration"))
return KT_QXmlStreamNamespaceDeclaration;
break;
case 32:
break; // <=
if (!type.compare(qPos, 32, "QPatternist::Item::Iterator::Ptr"))
return KT_QPatternist_Item_Iterator_Ptr;
case 34:
break; // <=
if (!type.compare(qPos, 34, "QPatternist::ItemSequenceCacheCell"))
return KT_QPatternist_ItemSequenceCacheCell;
case 37:
break; // <=
if (!type.compare(qPos, 37, "QNetworkHeadersPrivate::RawHeaderPair"))
return KT_QNetworkHeadersPrivate_RawHeaderPair;
if (!type.compare(qPos, 37, "QPatternist::AccelTree::BasicNodeData"))
return KT_QPatternist_AccelTree_BasicNodeData;
break;
}
return KT_Unknown;
}
Please note the 32, 34, and 37 branches of the switch statement above. Honestly, I can't think of any reason for using break; right before the executable code.
I thought that they were just temporary kludges. But no, they added at the same time as this code. According to the history, this code is about 14 years old. If any readers can suggest what exactly is going on here — you're welcome in the comments.
The analyzer points you to this code fragment by issuing the warning:
V779 [CWE-561, CERT-MSC12-C] Unreachable code detected. It is possible that an error is present. symbolgroupvalue.cpp 1565
Writing the correct data models is one of the trickiest things for Qt newcomers. And for a good reason. There are a lot of ways to screw this up. There are so many of them that even the Qt Creator developers have been trapped :)
The file: cppquickfixes:4676
QVariant data(int column, int role) const override
{
if (role == Qt::DisplayRole && column == NameColumn)
return m_memberInfo->data.memberVariableName;
if ( role == Qt::CheckStateRole && column > 0
&& column <= static_cast<int>(std::size(ColumnFlag)))
{
return m_memberInfo->requestedFlags & ColumnFlag[column] ? Qt::Checked :
Qt::Unchecked;
}
return {};
}
At first glance, you may think that the code is OK... But what if you look into the definition of the ColumnFlag enumeration? I added comments so you don't have to calculate yourself.
The file: cppquickfixes:4661
using Flag = GenerateGetterSetterOp::GenerateFlag;
constexpr static Flag ColumnFlag[] = {
Flag::Invalid, // 0
Flag::GenerateGetter, // 1
Flag::GenerateSetter, // 2
Flag::GenerateSignal, // 3
Flag::GenerateReset, // 4
Flag::GenerateProperty, // 5
Flag::GenerateConstantProperty, // 6
};
I think this hint helps understand what the issue is: you can get the overrun of the ColumnFlag array if you pass column equal to 7 to the function. Let's thank for it the lurking <= in the maximum value check. The correct code looks like this:
if ( role == Qt::CheckStateRole
&& column > 0
&& column < static_cast<int>(std::size(ColumnFlag)))
{
....
}
Do you remember that there are a lot of ways to screw up? In the same model, a check of the following form has also been copied several times:
The file: cppquickfixes:4693
bool setData(int column, const QVariant &data, int role) override
{
if (column < 1 || column > static_cast<int>(std::size(ColumnFlag)))
return false;
if (role != Qt::CheckStateRole)
return false;
if (!(m_memberInfo->possibleFlags & ColumnFlag[column]))
return false;
....
}
The error is the same, but now it is with the operator >. I've found all these things with the following warnings:
If you still think that these are one-off issues, I'll drop the small snippet as a final example, and we'll move on.
The file: defaultannotations.cpp:27
QVariant DefaultAnnotationsModel::data(const QModelIndex &index, int role) const
{
const auto row = static_cast<size_t>(index.row());
if (!index.isValid() || m_defaults.size() < row)
return {};
auto &item = m_defaults[row];
switch (role)
{
....
}
return {};
}
V557. [CWE-119, CERT-ARR30-C] Array overrun is possible. The 'row' index is pointing beyond array bound. defaultannotations.cpp 33
Almost all of us have a fav type of errors. For me, it's what's called "choice without choice" (Am I not the only one calling it that, though?) The point is that you can write as complex a condition as you want and still have the same result because of the code duplication in different branches. Qt Creator has delighted me with a couple of these issues.
The file: nodemetainfo.cpp:1834
QString NodeMetaInfo::componentFileName() const
{
if constexpr (!useProjectStorage())
{
if (isValid())
{
return m_privateData->componentFileName();
}
}
else
{
if (isValid())
{
return m_privateData->componentFileName();
}
}
return {};
}
Honestly, I don't know what the original concept of the code was, but I like the fact that the compiler deletes the irrelevant branch during the project build. Although it seems like nothing is wrong here, it's worth checking out the warning.
V523 [CWE-691] The 'then' statement is equivalent to the 'else' statement. nodemetainfo.cpp 1840
You can't tell for sure if it's an error or not, and this is really frustrating. Even nearby comments don't usually help. For example, as it's shown here:
The file: debuggerkitaspect.cpp:358
// We have an executable path.
FilePath fileName = FilePath::fromUserInput(binary);
if (item.command() == fileName)
{
// And it's is the path of this item.
level = std::min(item.matchTarget(tcAbi), DebuggerItem::MatchesSomewhat);
}
else
{
// This item does not match by filename, and is an unlikely candidate.
// However, consider using it as fallback if the tool chain fits.
level = std::min(item.matchTarget(tcAbi), DebuggerItem::MatchesSomewhat);
}
V523 [CWE-691] The 'then' statement is equivalent to the 'else' statement. debuggerkitaspect.cpp 362
Here, the errors are really difficult to notice at all. That's why they can live in the code for a very long time. Such errors often occur due to banal copy-pasting or "local" refactoring.
Unfortunately, it's uneasy to fight with such errors. Errors can easily slip through code reviews even for experienced programmers. Do you not trust me, though? Well, your loss. My colleague just recently wrote a note about a little error that even several programmers didn't see at once. It's better to delegate the search for such errors to an analyzer. At least its roboeyes aren't blurred. :)
By the way, I'd like to know if you have any favorite or hated error patterns. Is that a trifle? Or is it a bug that needs hours to be caught? Share these patterns in the comments — it will be fun to read and discuss. :)
Not a super cool bug, but I liked the context in which I found it. Look: there is an error in the precedence (of operations) in the code for checking the precedence (of something). There is even a ready-made error message. All the work was done for me. :)
The file: connectionmodel.cpp:2032
if (int firstError = checkOrder() != -1)
{
setInvalid(tr("Invalid order at %1").arg(firstError), firstError);
return;
}
Okay, not all of it. I still should tell you what's wrong here since the code doesn't look bad.
The trouble is in the condition: the 'A = B == C' expression is actually evaluated as 'A = (B == C)''. Especially here, the checkOrder function value is evaluated, and it returns the position where the error occurs. The value is compared to -1, and the result (0 or 1) is written to the variable. Shall I say that, in such a case, the error message will record a useless 1 instead of the normal position? Let's look at the warning:
V593 [CWE-783, CERT-EXP00-C] Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. connectionmodel.cpp 2032
Since we have a C++17 project here, why not use the if statement with the initializer and rewrite the code in the following way:
if (int firstError = checkOrder(); firstError != -1)
{
setInvalid(tr("Invalid order at %1").arg(firstError), firstError);
return;
}
Now, you certainly can't mix up the operation precedence.
I suggest we should take a little break and look at a very simple error. Although it's a simple error, it has been living in the project code for more than 4 years. The point is that developers lost the return instruction or assignment to the boundingRect variable.
The file: quickitemnodeinstance.cpp:576
QRectF QuickItemNodeInstance::boundingRectWithStepChilds(
QQuickItem *parentItem) const
{
QRectF boundingRect = parentItem->boundingRect();
boundingRect = boundingRect.united(QRectF(QPointF(0, 0), size()));
for (QQuickItem *childItem : parentItem->childItems())
{
....
}
if (boundingRect.isEmpty())
QRectF{0, 0, 640, 480}; // <=
return boundingRect;
}
V607 Ownerless expression 'QRectF { 0, 0, 640, 480 }'. quickitemnodeinstance.cpp 592
Let's start with the code at once, because otherwise it may be unclear what we're talking about:
The file: common.h:15
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wc++11-narrowing"
# include <wdbgexts.h>
# pragma clang diagnostic pop
#else
# pragma warning( disable : 4838 )
# include <wdbgexts.h>
# pragma warning( default : 4838 )
#endif // __clang__
Here, developers try to suppress the compiler warning about narrowing conversions in a third-party header file. Nothing is wrong with it overall, but the suppression implementation raises some questions. The issue is that using #pragma warning(default : X) doesn't recover the warning state that was previously turned off with #pragma warning(disable: X). Instead, it returns it to the default state, which isn't always correct.
The analyzer properly warns about it to avoid issues:
V665 [CERT-MSC00-C] Possibly, the usage of '#pragma warning(default: X)' is incorrect in this context. The '#pragma warning(push/pop)' should be used instead. Check lines: 21, 23. common.h 23
By the way, you can find a very good example of how to step on a rake in the documentation for the V665 diagnostic rule. I'll allow myself to quote it:
Imagine that a file is compiled with the /Wall switch used. The C4061 warning must be generated in this case. If you add the "#pragma warning(default : 4061)" directive, this warning will not be displayed, as it is turned off by default.
The most interesting thing is that, in the case of the Clang compiler, developers have done everything correctly—they've used the push/pop bindings. Let's make a small code fix, so we won't have issues with other compilers during the build:
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wc++11-narrowing"
# include <wdbgexts.h>
# pragma clang diagnostic pop
#else
# pragma warning( push )
# pragma warning( disable : 4838 )
# include <wdbgexts.h>
# pragma warning( pop )
#endif // __clang__
It's one of the most frequent mistakes that C++ programmers make from time to time. Even if you read the title of the paragraph, maybe you look at the constructor of the Editor class and think, "What's wrong here...?"
The file: compilerexplorereditor.cpp:773
Editor::Editor(TextEditorActionHandler &actionHandler)
: m_document(new JsonSettingsDocument(&m_undoStack))
{
setContext(Core::Context(Constants::CE_EDITOR_ID));
setWidget(new EditorWidget(m_document, &m_undoStack, actionHandler));
connect(&m_undoStack, &QUndoStack::canUndoChanged, this,
[&actionHandler] { actionHandler.updateActions(); });
connect(&m_undoStack, &QUndoStack::canRedoChanged, this,
[&actionHandler] { actionHandler.updateActions(); });
}
To be completely honest, I'll add a class declaration:
The file: compilerexplorereditor.h:237
class Editor : public Core::IEditor
{
public:
Editor(TextEditor::TextEditorActionHandler &actionHandler);
~Editor();
Core::IDocument *document() const override { return m_document.data(); }
QWidget *toolBar() override;
QSharedPointer<JsonSettingsDocument> m_document;
QUndoStack m_undoStack;
std::unique_ptr<QToolBar> m_toolBar;
};
Have you found it? If you have, shouldn't you challenge yourself in the quiz by Sergei Kushnirenko? I've heard that it has even more tricky questions... But shh, it's a secret. :)
If the error is beyond your grasp, I'll explain it. According to the C++ standard, class members in a member initializer list should be listed in the order that they're defined in the class. It turns out that the m_undoStack field will be initialized after it participates in the initialization of the m_document object, which can easily confuse a third-party developer.
A quick glance through the code shows that now the uninitialized object is not being used ahead of time. But, let's imagine that a new developer will need to limit the size of the m_undoStack history. Why not do it in the JsonSettingsDocument constructor, though?
The file: compilerexplorereditor.cpp:118
JsonSettingsDocument::JsonSettingsDocument(QUndoStack *undoStack)
: m_undoStack(undoStack)
{
m_undoStack->setUndoLimit(50); // Ops... Added for example
....
}
All in all, it's not a bad idea. It's sad that such code causes undefined behavior. Even checking the pointer before dereferencing won't save you; it's valid. Just the object it points to hasn't been initialized yet. That's why it's better to fix the code. Especially since it's not difficult at all. There are several options, and the easiest one is to change the fields order in the class.
class Editor : public Core::IEditor
{
public:
Editor(TextEditor::TextEditorActionHandler &actionHandler);
~Editor();
Core::IDocument *document() const override { return m_document.data(); }
QWidget *toolBar() override;
QUndoStack m_undoStack; // <=
QSharedPointer<JsonSettingsDocument> m_document; // <=
std::unique_ptr<QToolBar> m_toolBar;
};
Here's a corresponding warning:
V670 [CWE-457, CERT-EXP53-CPP] The uninitialized class member 'm_undoStack' is used to initialize the 'm_document' member. Remember that members are initialized in the order of their declarations inside a class. compilerexplorereditor.cpp 774
The analyzer detected an exception thrown by a pointer. Developers usually throw exceptions by value, and intercept by reference. Throwing a pointer may lead to a case when an exception won't be caught. And it also makes the intercepting side to call the operator delete to destroy the created exception object.
The file: celliterator.cpp:53
CellIterator &CellIterator::operator-=(int n)
{
....
if (m_pos - n < 0)
throw new std::runtime_error("-= n too big!");
}
I ran my eyes over the project code and didn't find any places where exceptions were caught by pointer. Most likely, this was an error that would sooner or later cause the program to crash. Alas.
Throwing an exception by a pointer isn't an error in itself, but we're better off avoiding it, especially in the overused code (the class name hints at its reusability). And the analyzer will be a good helper with such errors:
V1022 [CWE-755] An exception was thrown by pointer. Consider throwing it by value instead. celliterator.cpp 59
At the end, I suggest you look at a stunner—technically, it's a bug, but here it doesn't hurt the code. This is a good example of how you can suddenly add a little bit of undefined behavior (UB) to your program.
The file: containers.cpp:18
static void *readPointerArray(ULONG64 address, unsigned count,
const SymbolGroupValueContext &ctx)
{
const unsigned pointerSize = SymbolGroupValue::pointerSize();
const ULONG allocSize = pointerSize * count;
ULONG bytesRead = 0;
void *data = new unsigned char[allocSize];
const HRESULT hr = ctx.dataspaces->ReadVirtual(address, data, allocSize,
&bytesRead);
if (FAILED(hr) || bytesRead != allocSize)
{
delete [] data; // <=
return 0;
}
return data;
}
"So, where's the error?", you may ask me. We select them via new and delete them via delete[], as stated in the article "Why do arrays have to be deleted via delete[] in C++." What's wrong again?
The issue is in the pointer to void (loss of the real type). From the point of view of the C++ standard, such an array deletion is undefined behavior. Here is quote $8.3.5/3 of the C++20 standard:
In the first alternative (delete object), if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined. In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
Here, we're lucky that memory is allocated to a trivial type, and it doesn't seem to cause major issues. If it was a class with a non-trivial destructor... What am I talking about, though? UB is UB. No one knows what will actually happen.
The analyzer protects against such errors as well:
Well, let's stop here. We saw enough bugs, but at the same time, I'd like to say that the project is quite well-written: fresh code, documentation, pretty good API (though not without sins). It's really good, especially for a project with such a long history and its own static analyzer.
By the way, I mentioned the gift... And here it is: for this article, we've made a promocode: QTCREATOR. It allows you to try the PVS-Studio analyzer with the plugin for Qt Creator on your own code. Download the analyzer at the link.