How easy it is to modernize C ++ code

Hello, Habr!



We bring to your attention the translation of a short practical article on dealing with redundant legacy in C ++ code. We hope it will be interesting.



Recently, the C ++ community has been actively promoting the use of new standards and the modernization of the existing code base. However, even before the C ++ 11 standard was released, renowned C ++ experts such as Andre Alexandrescu, Scott Myers, and Herb Sutter promoted generic C ++ programming, which they qualified as "modern C ++ design." AndrΓ© Alexandrescu put it this way:



Modern C ++ design defines and systematically uses generic components β€” highly flexible design artifacts that can be mixed and matched to produce rich behaviors in a small, orthogonal piece of code.


Three statements are interesting in this thesis:



  • Modern C ++ design defines and systematically uses generic components .
  • Very flexible design.
  • Get rich behaviors with a small, orthogonal piece of code.


Modernizing code written in C ++ is not limited to the introduction of new standards, but also involves the use of best practices that apply in any programming language to help improve the code base. First, let's discuss some simple steps to manually upgrade your code base. In the third section, we'll talk about automatic code upgrades.



Manually updating the source code



Let's take an algorithm as an example and try to modernize it. Algorithms are used for calculations, data processing and automatic derivation of conclusions. Programming an algorithm is sometimes a non-trivial task and depends on its complexity. In C ++, significant efforts are made to simplify implementation and increase the power of algorithms.

Let's try to modernize this implementation of the quicksort algorithm:



//  
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);
        }
}

      
      





After all, all algorithms have certain things in common:



  • Using a container for elements of a certain type and iterating over them.
  • Comparison of elements
  • Some operations on elements


In our implementation, the container is a raw array of integers, and we iterate over the increment and decrement operations by one. The comparison is performed using β€œ<”



and β€œ>”



, and we also perform some operations on the data, for example, swap them.



Let's try to improve each of these features of the algorithm:



Step 1: Changing Containers to Iterators



If we abandon generic containers, then we will be forced to use only elements of a certain type. To apply the same algorithm to other types, we will have to copy and paste the code. Generic containers solve this problem and allow you to use any kind of element. For example, in a quick sort algorithm, you can use it std::vector<T>



as a container instead of a raw array.



A raw array or std::vector



is just one of a variety of options to represent many elements. The same algorithm applies to a linked list, a queue, or any other container. When working with an iterator, it is best to abstract the container used.



An iterator is any object that, pointing to an element in a certain range, can iterate over all the elements of the given range using a set of operators (which includes at least the increment by one operator (++) and the dereference operator (*)). Iterators fall into five categories based on the function they perform: Input, Output, One-way iterator, Two-way iterator, and random access.



In our algorithm, we have to specify which iterator we will use. To do this, we need to identify which iterations we are using. The quicksort algorithm uses an iteration of increment by one and decrement by one. Hence, we need a bidirectional iterator. With iterators, you can define a method like this:



template< typename BidirectionalIterator >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last )
      
      





Step 2: generalize the comparator, if possible



Some algorithms have to process not only numbers, but also, for example, a string or a class. In this case, you need to make the comparator generalized; this will allow us to achieve greater generalization of the entire algorithm.



The quick sort algorithm can be applied to a list of strings as well. Accordingly, a generalized comparator is better suited to us.



Using the generalized comparator, you can modify the definition like this:



template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp )
      
      





Stage 3: Replace existing operations with standard ones



Most algorithms use repetitive operations such as min



, max



and swap



. When performing such operations, it is better not to reinvent the wheel and use the standard implementation that exists in the header <algorithm>



.



In our case, we can use the swap method from the STL standard library instead of creating our own method.



std::iter_swap( pivot, left );
      
      





And here is the modified result after these three steps:



#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 >()
                );
    }
      
      





This implementation has the following advantages:



  • Applicable to all kinds of elements.
  • The container can be a vector, set, list, or any other provided with a bidirectional iterator.
  • This implementation uses optimized and tested standard functions.


Automatic upgrade



It is interesting to automatically identify places where you can use certain features of C ++ 11 / C ++ 14 / C ++ 17 and, if conditions are favorable, automatically change the code. For such purposes, there is a full-featured clang-tidy tool used to automatically transform C ++ code written according to old standards. After this transformation, the code uses features from newer standards where appropriate.



Here are some areas where clang-tidy suggests code upgrades:



  • Overriding: Find places where you can add an override pointer for an instance function that overrides a virtual function in the base class without already having one
  • : 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.
  • : , , .


Developers who have mastered Clang can easily learn to use the clang-tidy tool. But when working with Visual C ++, as well as other compilers, you can use CppDepend , which includes clang-tidy.



All Articles