Les applications modernes sont construites à partir de bibliothèques tierces comme des blocs de construction. C'est normal et la seule option pour terminer le projet dans un délai raisonnable et avec un budget raisonnable. Cependant, prendre toutes les briques sans discernement n'est peut-être pas une si bonne idée. S'il y a plusieurs options, alors il est utile de prendre le temps d'analyser les bibliothèques ouvertes afin de choisir la meilleure qualité.
Collection "Bibliothèques C ++ impressionnantes avec en-tête uniquement"
L'histoire de cette écriture a commencé avec le podcast Cppcast " Cross Platform Mobile Telephony ". De là, j'ai appris l'existence de la liste " awesome-hpp ", qui répertorie un grand nombre de bibliothèques C ++ ouvertes, constituées uniquement de fichiers d'en-tête.
Cette liste m'a intéressé pour deux raisons. Tout d'abord, c'est l'occasion de reconstituer la base de projets pour tester notre analyseur PVS-Studio sur du code moderne. De nombreux projets sont écrits en C ++ 11, C ++ 14 et C ++ 17. Deuxièmement, c'est l'occasion d'écrire un article sur la vérification de ces projets.
Les projets sont petits, il y a donc peu de bugs dans chacun d'eux individuellement. De plus, il y a peu d'avertissements, car certaines erreurs ne peuvent être détectées que si des classes ou des fonctions de modèle sont instanciées dans du code personnalisé. Tant que ces classes et fonctions ne sont pas utilisées, il est souvent impossible de déterminer s'il y a une erreur ou non. Néanmoins, au total, il y a eu beaucoup d'erreurs, et j'en parlerai dans le prochain article. Cet article ne concerne pas les erreurs, mais un avertissement.
Pourquoi analyser
En utilisant des bibliothèques tierces, vous leur faites confiance inconditionnellement pour effectuer une partie du travail et des calculs. Le danger est que parfois les programmeurs choisissent une bibliothèque sans même penser que les erreurs peuvent contenir non seulement leur code, mais aussi le code de la bibliothèque elle-même. En conséquence, il existe des erreurs non évidentes et incompréhensibles qui peuvent se manifester de la manière la plus inattendue.
Le code des bibliothèques open source bien connues est bien débogué et la probabilité d'y rencontrer une erreur est bien moindre que dans un code similaire écrit par vous-même. Le problème est que toutes les bibliothèques ne sont pas largement utilisées et déboguées. Et c'est là que se pose la question de l'évaluation de leur qualité.
Pour clarifier les choses, regardons un exemple. Prenons la bibliothèque JSONCONS .
JSONCONS est une bibliothèque C ++, en-tête uniquement, pour la construction de formats de données JSON et de type JSON tels que CBOR.Une bibliothèque spécifique pour des tâches spécifiques. Cela peut bien fonctionner dans l'ensemble et vous ne verrez jamais de bugs. Mais Dieu nous en préserve, vous devez utiliser cet opérateur surchargé << = .
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;
}
Avertissement de l'analyseur PVS-Studio: V629 Pensez à inspecter l'expression «1 << k». Décalage de bits de la valeur 32 bits avec une extension ultérieure vers le type 64 bits. bigint.hpp 744 Si
je comprends bien, la fonction fonctionne avec de grands nombres qui sont stockés sous forme de tableau d'éléments 64 bits. Pour travailler avec certains bits, vous devez former un masque 64 bits:
uint64_t mask = (1 << k) - 1;
Mais ce masque n'est pas formé correctement. Puisque le littéral numérique 1 est de type int , le décalage de plus de 31 bits entraînera un comportement indéfini.
De la norme:Le masque variable peut être tout ce que vous voulez. Oui, je sais, théoriquement, tout peut arriver à cause d'UB. Mais en pratique, très probablement, nous parlons d'un résultat d'expression incorrect.
shift-expression << additive-expression
...
2. La valeur de E1 << E2 est E1 décalée à gauche des positions de bits E2; les bits vides sont remplis de zéros. Si E1 a un type non signé, la valeur du résultat est E1 * 2 ^ E2, modulo réduit un de plus que la valeur maximale représentable dans le type de résultat. Sinon, si E1 a un type signé et une valeur non négative, et E1 * 2 ^ E2 est représentable dans le type de résultat, alors c'est la valeur résultante; sinon, le comportement n'est pas défini.
Donc, nous avons une fonction qui ne peut pas être utilisée. Au contraire, cela ne fonctionnera que pour certains cas particuliers de la valeur de l'argument d'entrée. C'est un piège potentiel dans lequel un programmeur peut tomber. Le programme peut exécuter et réussir divers tests, puis refuser de manière inattendue l'utilisateur sur d'autres fichiers d'entrée.
Et une autre erreur peut être vue dans l' opérateur >> = .
Une question rhétorique. Devriez-vous faire confiance à cette bibliothèque?
Cela en vaut peut-être la peine. Après tout, il y a des erreurs dans n'importe quel projet. Cependant, cela vaut la peine d'être considéré: si ces erreurs existent, y en a-t-il d'autres qui pourraient entraîner une corruption de données désagréable? Ne serait-il pas préférable de privilégier la bibliothèque la plus populaire / testée s'il y en a plusieurs?
Un exemple peu convaincant? D'accord, prenons un autre. Prenons la bibliothèque mathématique universelle . On s'attend à ce que la bibliothèque fournisse la capacité de fonctionner avec des vecteurs. Par exemple, multipliez et divisez un vecteur par une valeur scalaire. Ok, voyons comment ces opérations sont implémentées. Multiplication:
template<typename Scalar>
vector<Scalar> operator*(double scalar, const vector<Scalar>& v) {
vector<Scalar> scaledVector(v);
scaledVector *= scalar;
return v;
}
Avertissement de l'analyseur PVS-Studio: V1001 La variable 'scaledVector' est affectée mais n'est pas utilisée à la fin de la fonction. vector.hpp 124
En raison d'une faute de frappe, ce n'est pas le nouveau conteneur scaledVector qui est renvoyé , mais le vecteur d'origine. La même erreur est dans l'opérateur de division. Facepalm.
Encore une fois, ces erreurs ne signifient rien séparément. Bien que non, cela indique que cette bibliothèque est peu utilisée et qu'il y a une forte probabilité qu'elle présente d'autres bogues graves non détectés.
Production. Si plusieurs bibliothèques offrent la même fonctionnalité, il vaut la peine de procéder à une analyse préliminaire de leur qualité et de choisir la plus testée et la plus fiable.
Comment analyser
Ok, nous voulons comprendre la qualité du code des bibliothèques, mais comment faire? Oui, ce n'est pas facile à faire. Vous ne pouvez pas simplement aller voir le code. Au contraire, vous pouvez regarder quelque chose, mais cela donnera peu d'informations. De plus, il est peu probable qu'un tel examen aide à évaluer la densité des erreurs dans le projet.
Revenons à la bibliothèque mathématique universelle mentionnée précédemment. Essayez de trouver l'erreur dans le code de cette fonction. En fait, vu le commentaire qui l'accompagne, je ne peux pas passer cet endroit :).
// 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);
}
Je suis sûr que, malgré le fait que j'ai suggéré qu'il y a une erreur dans ce code, il n'est pas facile de le trouver.
Si non trouvé, alors le voici. Avertissement PVS-Studio: V581 Les expressions conditionnelles des instructions 'if' situées côte à côte sont identiques. Lignes de contrôle: 789, 790. value.hpp 790
if (r1_sign) r1 = twos_complement(r1);
if (r1_sign) r2 = twos_complement(r2);
Une faute de frappe classique. Dans la seconde condition, la variable r2_sign doit être vérifiée .
En général, vous pouvez oublier la révision "manuelle" du code. Oui, un tel chemin est possible, mais il prend trop de temps.
Qu'est-ce que je suggère? Très simple. Utilisez l'analyse de code statique .
Vérifiez les bibliothèques que vous avez l'intention d'utiliser. Commencez à regarder les rapports et tout deviendra assez clair rapidement.
Vous n'avez même pas besoin d'une analyse approfondie et approfondie et vous n'avez pas besoin de filtrer les faux positifs. Il vous suffit de parcourir le rapport et d'examiner les avertissements. Les faux positifs dus au manque de paramètres peuvent simplement être patients et se concentrer sur les erreurs.
Cependant, les faux positifs peuvent également être pris en compte indirectement. Plus il y en a, plus le code est compliqué. En d'autres termes, il existe de nombreuses astuces dans le code qui confondent l'analyseur. Ils confondent également les personnes qui soutiennent le projet et, par conséquent, affectent négativement sa qualité.
Remarque. N'oubliez pas la taille du projet. Il y aura toujours plus d'erreurs dans un grand projet. Mais le nombre d'erreurs n'est pas du tout le même que la densité d'erreur. Tenez compte de cela lorsque vous prenez des projets de différentes tailles et faites des ajustements.
Quoi utiliser
Il existe de nombreux outils d'analyse de code statique. Je suggère naturellement d'utiliser l'analyseur PVS-Studio . Il est idéal à la fois pour une évaluation ponctuelle de la qualité du code et pour la recherche régulière et la correction de bogues.
Vous pouvez vérifier le code des projets en C, C ++, C # et Java. Le produit est exclusif. Cependant, une licence d'essai gratuite sera plus que suffisante pour évaluer la qualité de plusieurs bibliothèques open source.
Je vous rappelle également qu'il existe plusieurs options de licence gratuite de l'analyseur pour:
- étudiants ;
- projets open source ;
- projets fermés (vous devez ajouter des commentaires spéciaux au code);
- Microsoft MVP .
Conclusion
La méthodologie d'analyse de code statique est encore injustement sous-estimée par de nombreux programmeurs. Une raison possible en est l'expérience avec des outils de classe "linter" bruyants simples qui effectuent des vérifications très simples et, malheureusement, souvent pas très utiles.
Pour ceux qui doutent qu'il soit utile d'essayer d'implémenter un analyseur statique dans le processus de développement, les deux publications suivantes sont:
- Comment implémenter un analyseur de code statique dans un projet hérité et ne pas démotiver l'équipe .
- Raisons d'introduire l'analyseur de code statique PVS-Studio dans le processus de développement .
Merci pour votre attention, et je vous souhaite moins de bugs tant dans votre code que dans le code des bibliothèques utilisées :).
Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien de traduction: Andrey Karpov. Pourquoi il est important d'appliquer une analyse statique aux bibliothèques ouvertes que vous ajoutez à votre projet .