
Moderne Anwendungen werden aus Bibliotheken von Drittanbietern wie Bausteinen erstellt. Dies ist normal und die einzige Möglichkeit, das Projekt in angemessener Zeit und mit einem angemessenen Budget abzuschließen. Es ist jedoch möglicherweise keine so gute Idee, alle Steine wahllos zu nehmen. Wenn es mehrere Optionen gibt, ist es hilfreich, sich die Zeit zu nehmen, um offene Bibliotheken zu analysieren, um die qualitativ hochwertigste auszuwählen.
Sammlung "Fantastische C ++ - Bibliotheken nur für Header"
Die Geschichte dieses Schreibens begann mit dem Cppcast-Podcast " Cross Platform Mobile Telephony ". Daraus erfuhr ich von der Existenz der " awesome-hpp " -Liste, die eine große Anzahl offener C ++ - Bibliotheken auflistet, die nur aus Header-Dateien bestehen.
Diese Liste hat mich aus zwei Gründen interessiert. Erstens ist es eine Gelegenheit, die Basis von Projekten zum Testen unseres PVS-Studio-Analysators auf modernem Code aufzufüllen. Viele Projekte sind in C ++ 11, C ++ 14 und C ++ 17 geschrieben. Zweitens ist es eine Gelegenheit, einen Artikel über die Überprüfung dieser Projekte zu schreiben.
Die Projekte sind klein, daher gibt es nur wenige Fehler in jedem einzelnen. Außerdem gibt es nur wenige Warnungen, weil Einige Fehler können nur erkannt werden, wenn Vorlagenklassen oder -funktionen in benutzerdefiniertem Code instanziiert werden. Bis diese Klassen und Funktionen verwendet werden, ist es oft unmöglich herauszufinden, ob ein Fehler vorliegt oder nicht. Insgesamt gab es jedoch viele Fehler, über die ich im nächsten Artikel schreiben werde. In diesem Artikel geht es nicht um Fehler, sondern um eine Warnung.
Warum analysieren?
Durch die Verwendung von Bibliotheken von Drittanbietern vertrauen Sie unbedingt darauf, dass diese einen Teil der Arbeit und Berechnungen ausführen. Die Gefahr besteht darin, dass Programmierer manchmal eine Bibliothek auswählen, ohne zu glauben, dass Fehler nicht nur ihren Code, sondern auch den Code der Bibliothek selbst enthalten können. Infolgedessen gibt es nicht offensichtliche, unverständliche Fehler, die sich auf unerwartete Weise manifestieren können.
Der Code bekannter offener Bibliotheken ist gut getestet, und die Wahrscheinlichkeit, dass dort ein Fehler auftritt, ist viel geringer als bei ähnlichem Code, den Sie selbst geschrieben haben. Das Problem ist, dass nicht alle Bibliotheken weit verbreitet und debuggt sind. Und hier stellt sich die Frage nach der Bewertung ihrer Qualität.
Schauen wir uns zur Verdeutlichung ein Beispiel an. Nehmen wir die JSONCONS- Bibliothek .
JSONCONS ist eine C ++ - Nur-Header-Bibliothek zum Erstellen von JSON- und JSON-ähnlichen Datenformaten wie CBOR.Eine bestimmte Bibliothek für bestimmte Aufgaben. Es kann insgesamt gut funktionieren, und Sie werden nie Fehler darin sehen. Aber Gott bewahre, dass Sie diesen überladenen Operator << = verwenden müssen .
static constexpr uint64_t basic_type_bits = sizeof(uint64_t) * 8;
....
uint64_t* data()
{
return is_dynamic() ? dynamic_stor_.data_ : short_stor_.values_;
}
....
basic_bigint& operator<<=( uint64_t k )
{
size_type q = (size_type)(k / basic_type_bits);
if ( q ) // Increase common_stor_.length_ by q:
{
resize(length() + q);
for (size_type i = length(); i-- > 0; )
data()[i] = ( i < q ? 0 : data()[i - q]);
k %= basic_type_bits;
}
if ( k ) // 0 < k < basic_type_bits:
{
uint64_t k1 = basic_type_bits - k;
uint64_t mask = (1 << k) - 1; // <=
resize( length() + 1 );
for (size_type i = length(); i-- > 0; )
{
data()[i] <<= k;
if ( i > 0 )
data()[i] |= (data()[i-1] >> k1) & mask;
}
}
reduce();
return *this;
}
Warnung zum PVS-Studio-Analysator: V629 Überprüfen Sie den Ausdruck '1 << k'. Bitverschiebung des 32-Bit-Werts mit anschließender Erweiterung auf den 64-Bit-Typ. bigint.hpp 744
Soweit ich weiß, funktioniert die Funktion mit großen Zahlen, die als Array von 64-Bit-Elementen gespeichert sind. Um mit bestimmten Bits arbeiten zu können, müssen Sie eine 64-Bit-Maske erstellen:
uint64_t mask = (1 << k) - 1;
Diese Maske ist jedoch nicht richtig geformt. Da das numerische Literal 1 vom Typ int ist , führt eine Verschiebung um mehr als 31 Bit zu einem undefinierten Verhalten.
Aus dem Standard:Die variable Maske kann beliebig sein. Ja, ich weiß, theoretisch kann wegen UB alles passieren. In der Praxis handelt es sich jedoch höchstwahrscheinlich um ein falsches Ausdrucksergebnis.
Verschiebungsausdruck << additiver Ausdruck
...
2. Der Wert von E1 << E2 ist E1 linksverschobene E2-Bitpositionen; Leerzeichen werden mit Null gefüllt. Wenn E1 einen vorzeichenlosen Typ hat, ist der Wert des Ergebnisses E1 * 2 ^ E2, modulo um eins mehr als der im Ergebnistyp darstellbare Maximalwert. Wenn andernfalls E1 einen vorzeichenbehafteten Typ und einen nicht negativen Wert hat und E1 * 2 ^ E2 im Ergebnistyp darstellbar ist, ist dies der resultierende Wert. Andernfalls ist das Verhalten undefiniert.
Wir haben also eine Funktion, die nicht verwendet werden kann. Vielmehr funktioniert es nur für einige Sonderfälle des Werts des Eingabearguments. Dies ist eine potenzielle Falle, in die ein Programmierer geraten kann. Das Programm kann verschiedene Tests ausführen und bestehen und den Benutzer dann unerwartet für andere Eingabedateien ablehnen.
Ein weiterer Fehler ist im Operator >> = zu sehen .
Eine rhetorische Frage. Sollten Sie dieser Bibliothek vertrauen?
Vielleicht lohnt es sich. Schließlich gibt es in jedem Projekt Fehler. Es lohnt sich jedoch zu überlegen: Wenn diese Fehler vorliegen, gibt es andere, die zu einer bösen Datenkorruption führen können? Wäre es nicht besser, der populäreren / getesteten Bibliothek den Vorzug zu geben, wenn es mehrere gibt?
Ein nicht überzeugendes Beispiel? Okay, lass uns noch einen holen. Nehmen wir die Universal Math Library . Es wird erwartet, dass die Bibliothek die Möglichkeit bietet, Vektoren zu bearbeiten. Multiplizieren und dividieren Sie beispielsweise einen Vektor durch einen Skalarwert. Ok, mal sehen, wie diese Operationen implementiert werden. Multiplikation:
template<typename Scalar>
vector<Scalar> operator*(double scalar, const vector<Scalar>& v) {
vector<Scalar> scaledVector(v);
scaledVector *= scalar;
return v;
}
Warnung des PVS-Studio-Analysators: V1001 Die Variable 'scaledVector' wird zugewiesen, aber am Ende der Funktion nicht verwendet. vector.hpp 124
Aufgrund eines Tippfehlers wird nicht der neue Container scaledVector zurückgegeben , sondern der ursprüngliche Vektor. Der gleiche Fehler liegt im Divisionsoperator. Gesichtspalme.
Auch diese Fehler bedeuten nichts separat. Obwohl nein, ist dies ein Hinweis darauf, dass diese Bibliothek nicht ausgelastet ist und es eine hohe Wahrscheinlichkeit gibt, dass andere schwerwiegende unbemerkte Fehler darin sind.
Ausgabe. Wenn mehrere Bibliotheken dieselbe Funktionalität bieten, lohnt es sich, eine vorläufige Analyse ihrer Qualität durchzuführen und die am besten getestete und zuverlässigste auszuwählen.
Wie zu analysieren
Ok, wir wollen die Qualität des Bibliothekscodes verstehen, aber wie geht das? Ja, das ist nicht einfach. Sie können nicht einfach den Code sehen. Sie können sich eher etwas ansehen, aber es gibt nur wenige Informationen. Darüber hinaus ist es unwahrscheinlich, dass eine solche Überprüfung dazu beiträgt, die Fehlerdichte im Projekt zu bewerten.
Kehren wir zur zuvor erwähnten universellen Mathematikbibliothek zurück. Versuchen Sie, den Fehler im Code dieser Funktion zu finden. Wenn ich den dazugehörigen Kommentar sehe, komme ich an diesem Ort nicht vorbei :).
// subtract module using SUBTRACTOR: CURRENTLY BROKEN FOR UNKNOWN REASON

template<size_t fbits, size_t abits>
void module_subtract_BROKEN(const value<fbits>& lhs, const value<fbits>& rhs,
value<abits + 1>& result) {
if (lhs.isinf() || rhs.isinf()) {
result.setinf();
return;
}
int lhs_scale = lhs.scale(),
rhs_scale = rhs.scale(),
scale_of_result = std::max(lhs_scale, rhs_scale);
// align the fractions
bitblock<abits> r1 = lhs.template nshift<abits>(lhs_scale-scale_of_result+3);
bitblock<abits> r2 = rhs.template nshift<abits>(rhs_scale-scale_of_result+3);
bool r1_sign = lhs.sign(), r2_sign = rhs.sign();
if (r1_sign) r1 = twos_complement(r1);
if (r1_sign) r2 = twos_complement(r2);
if (_trace_value_sub) {
std::cout << (r1_sign ? "sign -1" : "sign 1") << " scale "
<< std::setw(3) << scale_of_result << " r1 " << r1 << std::endl;
std::cout << (r2_sign ? "sign -1" : "sign 1") << " scale "
<< std::setw(3) << scale_of_result << " r2 " << r2 << std::endl;
}
bitblock<abits + 1> difference;
const bool borrow = subtract_unsigned(r1, r2, difference);
if (_trace_value_sub) std::cout << (r1_sign ? "sign -1" : "sign 1")
<< " borrow" << std::setw(3) << (borrow ? 1 : 0) << " diff "
<< difference << std::endl;
long shift = 0;
if (borrow) { // we have a negative value result
difference = twos_complement(difference);
}
// find hidden bit
for (int i = abits - 1; i >= 0 && difference[i]; i--) {
shift++;
}
assert(shift >= -1);
if (shift >= long(abits)) { // we have actual 0
difference.reset();
result.set(false, 0, difference, true, false, false);
return;
}
scale_of_result -= shift;
const int hpos = abits - 1 - shift; // position of the hidden bit
difference <<= abits - hpos + 1;
if (_trace_value_sub) std::cout << (borrow ? "sign -1" : "sign 1")
<< " scale " << std::setw(3) << scale_of_result << " result "
<< difference << std::endl;
result.set(borrow, scale_of_result, difference, false, false, false);
}
Ich bin sicher, obwohl ich vorgeschlagen habe, dass dieser Code einen Fehler enthält, ist es nicht einfach, ihn zu finden.
Wenn nicht, hier ist es. PVS-Studio- Warnung : V581 Die bedingten Ausdrücke der nebeneinander angeordneten if-Anweisungen sind identisch. Überprüfen Sie die Zeilen: 789, 790. value.hpp 790
if (r1_sign) r1 = twos_complement(r1);
if (r1_sign) r2 = twos_complement(r2);
Ein klassischer Tippfehler. In der zweiten Bedingung sollte die Variable r2_sign überprüft werden .
Im Allgemeinen können Sie die "manuelle" Codeüberprüfung vergessen. Ja, ein solcher Weg ist möglich, aber unangemessen zeitaufwändig.
Was schlage ich vor? Sehr einfach. Verwenden Sie die statische Code-Analyse .
Überprüfen Sie die Bibliotheken, die Sie verwenden möchten. Schauen Sie sich die Berichte an und alles wird schnell genug klar.
Sie benötigen nicht einmal eine gründliche Analyse und müssen keine Fehlalarme herausfiltern. Sie müssen nur den Bericht durchgehen und die Warnungen untersuchen. False Positives aufgrund fehlender Einstellungen können einfach geduldig sein und sich auf Fehler konzentrieren.
Fehlalarme können jedoch auch indirekt berücksichtigt werden. Je mehr es gibt, desto chaotischer ist der Code. Mit anderen Worten, der Code enthält viele Tricks, die den Analysator verwirren. Sie verwirren auch die Menschen, die das Projekt unterstützen, und beeinträchtigen infolgedessen dessen Qualität.
Hinweis. Vergessen Sie nicht die Projektgröße. Ein großes Projekt wird immer mehr Fehler haben. Die Anzahl der Fehler entspricht jedoch keineswegs der Fehlerdichte. Berücksichtigen Sie dies, wenn Sie Projekte unterschiedlicher Größe aufnehmen und Anpassungen vornehmen.
Was zu verwenden
Es gibt viele statische Code-Analyse-Tools. Ich empfehle natürlich die Verwendung des PVS-Studio- Analysators . Es eignet sich sowohl für eine einmalige Bewertung der Codequalität als auch für die regelmäßige Suche und Behebung von Fehlern.
Sie können den Code von Projekten in C, C ++, C # und Java überprüfen. Das Produkt ist urheberrechtlich geschützt. Eine kostenlose Testlizenz ist jedoch mehr als ausreichend, um die Qualität mehrerer Open Source-Bibliotheken zu bewerten.
Ich erinnere Sie auch daran, dass es mehrere Optionen für die kostenlose Lizenzierung des Analysators gibt für:
- Studenten ;
- Open Source-Projekte ;
- geschlossene Projekte (Sie müssen dem Code spezielle Kommentare hinzufügen);
- Microsoft MVP .
Fazit
Die Methodik der statischen Code-Analyse wird von vielen Programmierern immer noch zu Unrecht unterschätzt. Ein möglicher Grund dafür ist die Erfahrung mit einfachen lauten "Linter" -Klassenwerkzeugen, die sehr einfache und leider oft nicht sehr nützliche Überprüfungen durchführen.
Für diejenigen, die bezweifeln, ob es sich lohnt, einen statischen Analysator im Entwicklungsprozess zu implementieren, sind die folgenden zwei Veröffentlichungen:
- So implementieren Sie einen statischen Code-Analysator in einem Legacy-Projekt und demotivieren das Team nicht .
- Gründe, den statischen Code-Analysator PVS-Studio in den Entwicklungsprozess einzuführen .
Vielen Dank für Ihre Aufmerksamkeit und ich wünsche Ihnen weniger Fehler sowohl in Ihrem Code als auch im Code der verwendeten Bibliotheken :).

Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Übersetzungslink: Andrey Karpov. Warum es wichtig ist, statische Analysen für offene Bibliotheken anzuwenden, die Sie Ihrem Projekt hinzufügen .