17.3. Refactoring to Algorithms¶
The primary objective of refactoring is to improve code. Those improvements might take many forms. In this section we are going to focus on refactoring a pair of functions that at first glance do not appear to be doing the same thing. However, we will see the similarities and how refactoring is accomplished, step-by-step.
Given two functions, each sums the values provided.
The first function adds all of the integers in a raw array:
int sum(int array[], int n) {
int sum = 0;
for (int i = 0; i < n; ++i ) {
sum += array[i];
}
return sum;
}
The second adds all of the elements in a simple, home-grown linked list.
// create a simple node in a linked list
struct node {
int value = 0;
node* next = nullptr;
};
int sum(node* first) {
int s = 0;
while (first) { // first not false or zero
s += first->value;
first = first->next;
}
return s;
}
How can we generalize and combine these two functions into one? We can rewrite both functions in a form of pseudo-code.
// we need a generic type 'T'
T sum(/* data */ ) // somehow parameterize this
{
T s = 0;
while (/* not at end */ ) { // loop through all elements
s = s + /* get value */; // compute sum
/* get next data element */;
}
return s;
}
We need several generic operations on data:
Determine if we are not at end of data
Get value
Get next element
The STL style supports both data structures.
Like find, we define a pair of iterators. first
and last
.
The iterator type should support the requirements of
InputIterator.
A separate template parameter for the initial sum finishes the signature.
The value must be a regular type and the dereferenced iterator must be convertible to the value type.
The function signature becomes:
template <typename InputIt, typename T>
// requires: InputIt is convertible to T when dereferenced
// && InputIt is EqualityComparable
// && T is Regular
T sum (InputIt first, InputIt last, T value) {
The main loop checks whether we should continue and accumulates the sum:
while (first != last) {
value = value + *first;
++first;
}
And we can use this algorithm with either a raw array or a linked list.
See this example running step-by-step
17.3.1. Removing a final assumption¶
Can we make sum
even more generic?
Sum still has a hard-coded assumption that addition ( the operator+
function)
is the operation that we always want to perform.
Might we want to perform any binary operation on a sequence? If yes, then we can add one more template parameter allowing callers to pass in a function pointer (or equivalent).
The function signature becomes:
template <typename InputIt, typename T, typename BinaryOp>
// requires: InputIt is convertible to T when dereferenced
// && InputIt is EqualityComparable
// && T is Regular
T accumulate (InputIt first,
InputIt last,
T value,
BinaryOp op) {
The main loop replaces the explicit +
with
a call to a provided binary operator:
value = op(value, *first);
This could be addition: operator+
, but can now support
any binary operation that the type T
supports.
A default operation can be provided with a supporting template that calls accumulate with plus.
template <typename InputIt, typename T>
T accumulate (InputIt first, InputIt last, T value) {
return accumulate(first, last, value, std::plus<T>());
}
See this example running step-by-step
Note that
we did not pass +
or *
to a function.
The symbol +
is not a type.
The parameter passed through BinaryOp op
must be a valid type.
A function can take a pointer or a type as a parameter. Function objects passed as parameters must satisfy the requirements of function. Lambda expressions, function objects, and functions pointers are all acceptable. The STL has a large collection of operator types that can be passed to functions.
More to Explore
From CPP Core Guidelines
T.2: Use templates to express algorithms that apply to many argument types