
Os aplicativos modernos são construídos a partir de bibliotecas de terceiros, como blocos de construção. Isso é normal e a única opção para concluir o projeto em um período de tempo razoável e com um orçamento razoável. No entanto, pegar todos os tijolos indiscriminadamente pode não ser uma ideia tão boa. Se houver várias opções, é útil analisar as bibliotecas abertas para escolher a de melhor qualidade.
Coleção "Bibliotecas C ++ incríveis apenas de cabeçalho"
A história desta escrita começou com o podcast Cppcast " Cross Platform Mobile Telephony ". Com ela, aprendi sobre a existência da lista " awesome-hpp ", que lista um grande número de bibliotecas C ++ abertas, consistindo apenas de arquivos de cabeçalho.
Essa lista me interessou por dois motivos. Em primeiro lugar, é uma oportunidade de reabastecer a base de projetos para testar nosso analisador PVS-Studio em código moderno. Muitos projetos são escritos em C ++ 11, C ++ 14 e C ++ 17. Em segundo lugar, é uma oportunidade de escrever um artigo sobre como verificar esses projetos.
Os projetos são pequenos, então existem poucos bugs em cada um individualmente. Além disso, existem alguns avisos, porque alguns erros só podem ser detectados se as classes ou funções de modelo forem instanciadas no código personalizado. Até que essas classes e funções sejam usadas, geralmente é impossível descobrir se há um erro ou não. No entanto, no total, houve muitos erros, e irei escrever sobre eles no próximo artigo. Este artigo não é sobre erros, mas sobre um aviso.
Por que analisar
Ao usar bibliotecas de terceiros, você confia incondicionalmente nelas para fazer parte do trabalho e cálculos. O perigo é que às vezes os programadores escolhem uma biblioteca sem nem mesmo pensar que os erros podem conter não apenas seu código, mas também o código da própria biblioteca. Como resultado, existem erros não óbvios e incompreensíveis que podem se manifestar da maneira mais inesperada.
O código de bibliotecas de código aberto conhecidas é bem depurado e a probabilidade de ocorrer um erro é muito menor do que em um código semelhante escrito por você. O problema é que nem todas as bibliotecas são amplamente utilizadas e depuradas. E é aí que surge a questão de avaliar sua qualidade.
Para tornar isso mais claro, vejamos um exemplo. Vamos pegar a biblioteca JSONCONS .
JSONCONS é uma biblioteca C ++ apenas de cabeçalho para a construção de formatos de dados JSON e JSON, como CBOR.Uma biblioteca específica para tarefas específicas. Pode funcionar bem no geral e você nunca verá bugs nele. Mas Deus me livre de usar este operador sobrecarregado << = .
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;
}
Aviso do analisador PVS-Studio: V629 Considere inspecionar a expressão '1 << k'. Mudança de bit do valor de 32 bits com uma expansão subsequente para o tipo de 64 bits. bigint.hpp 744
Pelo que entendi, a função funciona com grandes números que são armazenados como uma matriz de elementos de 64 bits. Para trabalhar com certos bits, você precisa formar uma máscara de 64 bits:
uint64_t mask = (1 << k) - 1;
Mas esta máscara está formada incorretamente. Como o literal numérico 1 é do tipo int , deslocá-lo em mais de 31 bits resultará em um comportamento indefinido.
Do padrão:A máscara variável pode ser o que você quiser. Sim, eu sei, teoricamente tudo pode acontecer por causa do UB. Mas, na prática, muito provavelmente, estamos falando de um resultado de expressão incorreto.
shift-expressão << expressão-aditiva
...
2. O valor de E1 << E2 são as posições de bit E2 deslocadas para a esquerda; bits vagos são preenchidos com zeros. Se E1 tiver um tipo sem sinal, o valor do resultado é E1 * 2 ^ E2, módulo reduzido um a mais do que o valor máximo representável no tipo de resultado. Caso contrário, se E1 tiver um tipo com sinal e valor não negativo, e E1 * 2 ^ E2 for representável no tipo de resultado, então esse é o valor resultante; caso contrário, o comportamento é indefinido.
Portanto, temos uma função que não pode ser usada. Em vez disso, funcionará apenas para alguns casos especiais do valor do argumento de entrada. Esta é uma armadilha potencial na qual um programador pode cair. O programa pode ser executado e passar por vários testes e, em seguida, recusar inesperadamente o usuário em outros arquivos de entrada.
Você também pode ver outro erro como este no operador >> = .
Uma pergunta retórica. Você deve confiar nesta biblioteca?
Talvez valha a pena. Afinal, existem erros em qualquer projeto. No entanto, vale a pena considerar: se esses erros existem, há outros que podem levar à corrupção de dados desagradável? Não é melhor dar preferência à biblioteca mais popular / testada se houver várias?
Um exemplo não convincente? Ok, vamos pegar outro. Vamos pegar a biblioteca universal de matemática . Espera-se que a biblioteca forneça a capacidade de operar com vetores. Por exemplo, multiplique e divida um vetor por um valor escalar. Ok, vamos ver como essas operações são implementadas. Multiplicação:
template<typename Scalar>
vector<Scalar> operator*(double scalar, const vector<Scalar>& v) {
vector<Scalar> scaledVector(v);
scaledVector *= scalar;
return v;
}
Aviso do analisador PVS-Studio: V1001 A variável 'scaledVector' é atribuída, mas não é usada no final da função. vector.hpp 124
Devido a um erro de digitação, não é o novo container scaledVector que é retornado , mas o vetor original. O mesmo erro ocorre no operador de divisão. Facepalm.
Novamente, esses erros não significam nada separadamente. Embora não, isso é uma dica de que essa biblioteca é subutilizada e há uma grande probabilidade de que haja outros bugs graves despercebidos nela.
Resultado. Se várias bibliotecas oferecem a mesma funcionalidade, vale a pena fazer uma análise preliminar de sua qualidade e escolher a mais testada e confiável.
Como analisar
Ok, queremos entender a qualidade do código das bibliotecas, mas como fazer isso? Sim, isso não é fácil de fazer. Você não pode simplesmente ir e ver o código. Em vez disso, você pode ver algo, mas isso fornecerá poucas informações. Além disso, é improvável que tal revisão ajude a avaliar a densidade de erros no projeto.
Voltemos à biblioteca matemática Universal mencionada anteriormente. Tente encontrar o erro no código desta função. Na verdade, vendo o comentário que o acompanha, não consigo passar por este lugar :).
// 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);
}
Tenho a certeza, apesar de ter sugerido que existe um erro neste código, não é fácil localizá-lo.
Se não, aqui está. Aviso do PVS-Studio: V581 As expressões condicionais das instruções 'if' situadas lado a lado são idênticas. Verifique as linhas: 789, 790. value.hpp 790
if (r1_sign) r1 = twos_complement(r1);
if (r1_sign) r2 = twos_complement(r2);
Um erro de digitação clássico. Na segunda condição, a variável r2_sign deve ser verificada .
Em geral, você pode esquecer a revisão "manual" do código. Sim, esse caminho é possível, mas é excessivamente demorado.
O que eu sugiro? Muito simples. Use a análise de código estático .
Verifique as bibliotecas que você pretende usar. Comece a olhar os relatórios e tudo ficará claro rapidamente.
Você nem mesmo precisa de uma análise profunda e completa e não precisa filtrar falsos positivos. Você apenas tem que ler o relatório e examinar os avisos. Os falsos positivos devido à falta de configurações podem simplesmente ser pacientes e focar nos erros.
No entanto, os falsos positivos também podem ser considerados indiretamente. Quanto mais existem, mais confuso é o código. Em outras palavras, existem muitos truques no código que confundem o analisador. Eles também confundem as pessoas que apóiam o projeto e, como resultado, afetam negativamente sua qualidade.
Nota. Não se esqueça do tamanho do projeto. Um grande projeto sempre terá mais bugs. Mas o número de erros não é igual à densidade do erro. Considere isso ao pegar projetos de tamanhos diferentes e fazer ajustes.
O que usar
Existem muitas ferramentas de análise de código estático. Naturalmente, sugiro usar o analisador PVS-Studio . É ótimo tanto para uma avaliação única da qualidade do código, quanto para pesquisas regulares e correção de bugs.
Você pode verificar o código dos projetos em C, C ++, C # e Java. O produto é proprietário. No entanto, uma licença de teste gratuita será mais do que suficiente para avaliar a qualidade de várias bibliotecas de código aberto.
Também lembro que existem várias opções de licenciamento gratuito do analisador para:
- alunos ;
- projetos de código aberto ;
- projetos fechados (você precisa adicionar comentários especiais ao código);
- Microsoft MVP .
Conclusão
A metodologia de análise estática de código ainda é subestimada indevidamente por muitos programadores. Uma possível razão para isso é a experiência com ferramentas "linter" simples e barulhentas, que realizam verificações muito simples e, infelizmente, muitas vezes não muito úteis.
Para quem tem dúvidas se vale a pena tentar implementar um analisador estático no processo de desenvolvimento, as duas publicações a seguir:
- Como implementar um analisador de código estático em um projeto legado e não desmotivar a equipe .
- Razões para introduzir o analisador de código estático PVS-Studio no processo de desenvolvimento .
Obrigado pela atenção e desejo menos bugs tanto no seu código quanto no código das bibliotecas utilizadas :).

Se você deseja compartilhar este artigo com um público que fala inglês, por favor, use o link de tradução: Andrey Karpov. Por que é importante aplicar análise estática para bibliotecas abertas que você adiciona ao seu projeto .