Recently, the C++ community has been promoting the use of the new standards to modernize existing codebases. However, even before the release of the C++11 standard, well-known C++ experts such as Andrei Alexandrescu, Scott Meyers, and Herb Sutter were already promoting generic programming under the banner of Modern C++ Design. Here’s what Andrei Alexandrescu says about Modern C++ Design:
Modern C++ Design defines and systematically uses generic components - highly flexible design artifacts that are mixable and matchable to obtain rich behaviors with a small, orthogonal body of code.
Three points in his statement stand out:
- Modern C++ Design defines and systematically uses generic components.
- Highly flexible design.
- Obtain rich behaviors with a small, orthogonal body of code.
Modernizing your C++ code is not only about using the new standards; we can also use some generic programming best practices to improve our codebase. Let’s first discover some easy steps to manually modernize our codebase, and in the second section we will explore how to modernize it automatically.
I - Modernize your source code manually
Let’s use an algorithm as an example and try to modernize it. Algorithms are used for calculation, data processing, and automated reasoning. Programming them is not always an easy task, and it depends on their complexity. In C++, considerable effort has been made to simplify their implementation and make them more powerful.
Let’s try to modernize this implementation of the quicksort algorithm:
// The partition function
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;
}
// The quicksort recursive function
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);
}
}At a high level, here are some common traits of algorithms:
- Using containers of a given element type and iterating through them.
- Comparison between elements.
- And of course, some processing of the elements.
In our implementation, the container is a raw array of int; we iterate by incrementing and decrementing. We compare using “<” and “>”, and we have some operations like swapping data.
Let’s try to improve each of these aspects:
Step 1: Replace containers with iterators
Using non-generic containers forces us to use a specific element type. To apply the same algorithm to other types, we have to copy and paste the code. Generic containers solve this issue by making it possible to use any element type; for example, for our quicksort algorithm, we can use std::vector<T> as the container instead of a raw array.
A raw array or an std::vector is just one possibility among many for representing a set of elements; we could also apply the same algorithm to a linked list, a queue or any other container. For this purpose, iterators are the best choice for abstracting away the underlying container.
An iterator is an object that points to an element in a range and can traverse the elements of that range using a set of operators (with at least the increment (++) and dereference (*) operators). Iterators are classified into five categories depending on the functionality they implement: Input, Output, Forward, Bidirectional and Random Access.
In our algorithm, we have to specify what kind of iterator to use. For that, we have to detect which iteration operations are used. For the quicksort algorithm, increment and decrement are applied. Therefore, a bidirectional iterator is required. Using iterators, we can define the method like this:
template< typename BidirectionalIterator >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last )Step 2: Make the comparator generic if possible
For some algorithms, the elements being processed are not necessarily numbers; they could be strings or class objects. In this case, making the comparator generic gives us a more reusable algorithm.
The quicksort algorithm could also be applied to a list of strings; therefore, it’s better to use a generic comparator.
After using a generic comparator, the definition could be modified like this:
template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp )Step 3: Replace custom operations with standard ones
Many algorithms use recurring operations such as min, max, and swap. For these operations, it’s better not to reinvent the wheel and instead use the standard implementations from the <algorithm> header.
In our case, we can use the swap method from the STL rather than creating our own specific method.
std::iter_swap( pivot, left );And here’s 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:
- It can be applied to many element types.
- The container could be a vector, set, list, or any other container with a bidirectional iterator.
- It uses well-optimized and tested standard functions.
II - Automatic modernization
It is useful to automatically detect places where C++11/C++14/C++17 features can be used and, if possible, change the code automatically. For such needs, clang-tidy is a standalone tool used to automatically convert C++ code written against old standards to use features of the newest C++ standard where appropriate.
Here are some cases where clang-tidy can detect opportunities to modernize the code:
- Override: Detect places where you can add the override specifier to member functions that override a virtual function in a base class and that don’t already have the specifier.
- Loop Convert: Detect loops like for(…; …; …) to replace them with the new range-based loops in C++11 and provide the corresponding range-based loop expression.
- Pass-By-Value: Detect const-ref parameters that would benefit from using the pass-by-value idiom.
- auto_ptr: Detect uses of the deprecated std::auto_ptr to replace them with std::unique_ptr.
- auto specifier: Detect places where the auto type specifier can be used in variable declarations.
- nullptr: Detect null literals to be replaced by nullptr where applicable.
- std::bind: The check finds uses of
std::bindand replaces simple uses with lambdas. Lambdas will use value-capture where required. - Deprecated headers: Some headers from the C library were deprecated in C++ and are no longer welcome in C++ codebases. Some have no effect in C++. For more details, refer to the C++14 Standard [depr.c.headers] section.
- std::shared_ptr: This check finds the creation of
std::shared_ptrobjects by explicitly calling the constructor and anewexpression, and replaces it with a call tostd::make_shared. - std::unique_ptr: This check finds the creation of
std::unique_ptrobjects by explicitly calling the constructor and anewexpression, and replaces it with a call tostd::make_unique, introduced in C++14. - raw string literals: This check selectively replaces string literals containing escaped characters with raw string literals.
Developers who use Clang can easily take advantage of clang-tidy. Visual C++ developers and users of other compilers can use CppDepend, which integrates clang-tidy.
