Chamamos sua atenção para a tradução de um pequeno artigo prático sobre como lidar com legado redundante em código C ++. Esperamos que seja interessante.
Recentemente, a comunidade C ++ tem promovido ativamente o uso de novos padrões e a modernização da base de código existente. No entanto, mesmo antes do lançamento do padrão C ++ 11, renomados especialistas em C ++, como Andre Alexandrescu, Scott Myers e Herb Sutter, promoveram a programação C ++ genérica, que eles qualificaram como "design C ++ moderno". André Alexandrescu colocou desta forma:
O design C ++ moderno define e usa sistematicamente componentes genéricos - artefatos de design altamente flexíveis que podem ser misturados e combinados para produzir comportamentos ricos em um pequeno trecho ortogonal de código.
Três afirmações são interessantes nesta tese:
- O design moderno do C ++ define e usa sistematicamente componentes genéricos .
- Design muito flexível .
- Obtenha comportamentos ricos com um pequeno trecho de código ortogonal .
A modernização do código escrito em C ++ não se limita à introdução de novos padrões, mas também envolve o uso das melhores práticas que se aplicam a qualquer linguagem de programação para ajudar a melhorar a base do código. Primeiro, vamos discutir algumas etapas simples para atualizar manualmente sua base de código. Na terceira seção, falaremos sobre atualizações automáticas de código.
Atualização manual do código-fonte
Vamos pegar um algoritmo como exemplo e tentar modernizá-lo. Algoritmos são usados para cálculos, processamento de dados e derivação automática de conclusões. Programar um algoritmo às vezes é uma tarefa não trivial e depende de sua complexidade. Em C ++, esforços significativos são feitos para simplificar a implementação e aumentar o poder dos algoritmos.
Vamos tentar modernizar esta implementação do algoritmo quicksort:
//
int partition(int* input,int p,int r){
int pivot = input[r];
while( p < r ){
while( input[p]< pivot )
p++;
while( input[r]> pivot )
r--;
if( input[p]== input[r])
p++;
elseif( p < r ){
int tmp = input[p];
input[p]= input[r];
input[r]= tmp;
}
}
return r;
}
//
void quicksort(int* input,int p,int r){
if( p < r ){
int j = partition(input, p, r);
quicksort(input, p, j-1);
quicksort(input, j+1, r);
}
}
Afinal, todos os algoritmos têm certas coisas em comum:
- Usando um contêiner para elementos de um determinado tipo e iterando sobre eles.
- Comparação de elementos
- Algumas operações em elementos
Em nossa implementação, o contêiner é uma matriz bruta de inteiros, e iteramos as operações de incremento e decremento em um. A comparação é realizada usando
“<”
e
“>”
, e também realizamos algumas operações nos dados, por exemplo, trocamos os dados.
Vamos tentar melhorar cada um desses recursos do algoritmo:
Etapa 1: Mudando Containers para Iteradores
Se abandonarmos os containers genéricos, seremos forçados a usar apenas elementos de um determinado tipo. Para aplicar o mesmo algoritmo a outros tipos, teremos que copiar e colar o código. Os recipientes genéricos resolvem este problema e permitem que você use qualquer tipo de elemento. Por exemplo, em um algoritmo de classificação rápida, você pode usá-lo
std::vector<T>
como um contêiner em vez de uma matriz bruta.
Matriz bruta ou
std::vector
é apenas uma de uma variedade de opções para representar muitos elementos. O mesmo algoritmo se aplica a uma lista vinculada, uma fila ou qualquer outro contêiner. Ao trabalhar com um iterador, é melhor abstrair o contêiner usado.
Um iterador é qualquer objeto que, apontando para um elemento em um determinado intervalo, pode iterar sobre todos os elementos de um determinado intervalo usando um conjunto de operadores (que inclui pelo menos o incremento de um operador (++) e o operador de desreferência (*)). Os iteradores se enquadram em cinco categorias com base na função que executam: entrada, saída, iterador unilateral, iterador bidirecional e acesso aleatório.
Em nosso algoritmo, temos que especificar qual iterador usaremos. Para fazer isso, precisamos identificar quais iterações estamos usando. O algoritmo quicksort usa uma iteração de incremento de um e decremento de um. Portanto, precisamos de um iterador bidirecional. Com iteradores, você pode definir um método como este:
template< typename BidirectionalIterator >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last )
Etapa 2: generalize o comparador, se possível
Alguns algoritmos precisam processar não apenas números, mas, por exemplo, uma string ou uma classe. Nesse caso, você precisa tornar o comparador generalizado; isso nos permitirá alcançar uma maior generalização de todo o algoritmo.
O algoritmo de classificação rápida também pode ser aplicado a uma lista de strings. Conseqüentemente, um comparador generalizado é mais adequado para nós.
Usando o comparador generalizado, você pode modificar a definição assim:
template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp )
Etapa 3: Substituir as operações existentes por outras padrão
A maioria dos algoritmos usa operações repetitivas como
min
,
max
e
swap
. Ao realizar tais operações, é melhor não reinventar a roda e usar a implementação padrão que existe no cabeçalho
<algorithm>
.
Em nosso caso, podemos usar o método de troca da biblioteca padrão STL em vez de criar nosso próprio método.
std::iter_swap( pivot, left );
E aqui está o resultado modificado após essas três etapas:
#include <functional>
#include <algorithm>
#include <iterator>
template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp ) {
if( first != last ) {
BidirectionalIterator left = first;
BidirectionalIterator right = last;
BidirectionalIterator pivot = left++;
while( left != right ) {
if( cmp( *left, *pivot ) ) {
++left;
} else {
while( (left != right) && cmp( *pivot, *right ) )
--right;
std::iter_swap( left, right );
}
}
--left;
std::iter_swap( pivot, left );
quick_sort( first, left, cmp );
quick_sort( right, last, cmp );
}
}
template< typename BidirectionalIterator >
inline void quick_sort( BidirectionalIterator first, BidirectionalIterator last ) {
quick_sort( first, last,
std::less_equal< typename std::iterator_traits< BidirectionalIterator >::value_type >()
);
}
Esta implementação tem as seguintes vantagens:
- Aplicável a todos os tipos de elementos.
- O contêiner pode ser um vetor, conjunto, lista ou qualquer outro fornecido com um iterador bidirecional.
- Esta implementação usa funções padrão otimizadas e testadas.
Atualização automática
É interessante identificar automaticamente os locais onde determinados recursos do C ++ 11 / C ++ 14 / C ++ 17 podem ser usados e, se as condições forem favoráveis, alterar automaticamente o código. Para tais propósitos, existe uma ferramenta clang-tidy cheia de recursos usada para transformar automaticamente o código C ++ escrito de acordo com padrões antigos. Após essa conversão, o código usa recursos de padrões mais novos quando apropriado.
Aqui estão algumas áreas em que o clang-tidy sugere atualizações de código:
- Substituindo: Encontre lugares onde você pode adicionar um ponteiro de substituição para uma função de instância que substitui uma função virtual na classe base sem já ter uma
- :
for(…; …; …)
, , , . - :
const-ref
, . auto_ptr
:std::auto_ptr
std::unique_ptr
.- -: ,
auto
. nullptr
: ,nullptr
, .std::bind
: std::bind , , . , , .- : C C++ . C++. C++ 14 [depr.c.headers].
std::shared_ptr
:std::shared_ptr
new
,std::make_shared
.std::unique_ptr
:std::shared_ptr
new
,std::make_unique
, C++14.- : , , .
Os desenvolvedores que dominam o Clang podem aprender facilmente a usar a ferramenta clang-tidy. Mas ao trabalhar com Visual C ++, bem como outros compiladores, você pode usar CppDepend , que inclui clang-tidy.