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(const int array[], std::size_t n) {
  int total = 0;
  for (int i = 0; i < n; ++i ) {
    total += array[i];
  }
  return total;
}

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

Example

The standard library algorithm style supports both data structures.

Like find, we define a pair of iterators. first and last. The iterator type should satisfy the C++20 std::input_iterator concept.

A separate template parameter for the initial sum finishes the signature.

The accumulator type must be movable and assignable from the result of the operation. The operation must be invocable with the accumulator and a dereferenced iterator.

The function signature becomes:

template <std::input_iterator InputIt, class T>
requires std::movable<T>
      && requires(T value, std::iter_reference_t<InputIt> element) {
           { value + element } -> std::convertible_to<T>;
         }
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;

Run It

And we can use this algorithm with either a raw array or a linked list.

 1#include <concepts>
 2#include <iostream>
 3#include <iterator>
 4#include <list>
 5
 6// accumulate the sum of values in range [first, last)
 7template <std::input_iterator InputIt, class T>
 8requires std::movable<T>
 9      && requires(T value, std::iter_reference_t<InputIt> element) {
10           { value + element } -> std::convertible_to<T>;
11         }
12T sum(InputIt first, InputIt last, T value) {
13  while (first != last) {
14    value = value + *first;
15    ++first;
16  }
17  return value;
18}
19
20int main() {
21  float values[] = {1, 1, 2, 3, 5, 8, 13, 21, 34};
22  float* end = values + sizeof(values) / sizeof(*values);
23  double total = sum(values, end, 0.0);
24  std::cout << "array sum = " << total << '\n';
25
26  std::list<float> linked_values = {1, 1, 2, 3, 5, 8, 13, 21, 34};
27  total = sum(linked_values.begin(), linked_values.end(), 0.0);
28  std::cout << "list sum = " << total << '\n';
29}

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 callable object such as a function pointer, lambda, or function object.

Example

The function signature becomes:

template <std::input_iterator InputIt, class T, class BinaryOp>
requires std::movable<T>
      && std::invocable<BinaryOp&, T, std::iter_reference_t<InputIt>>
      && std::assignable_from<
           T&, std::invoke_result_t<
                 BinaryOp&, T, std::iter_reference_t<InputIt>>>
T my_accumulate(InputIt first,
                InputIt last,
                T value,
                BinaryOp op) {

The main loop replaces the explicit + with a call to a provided binary operator:

  value = std::invoke(op, std::move(value), *first);

This could be addition, represented by std::plus<>, but can now support any binary operation that satisfies the callable requirements.

A default operation can be provided with an overload that calls my_accumulate with plus.

template <std::input_iterator InputIt, class T>
requires std::movable<T>
      && std::invocable<
           std::plus<>&, T, std::iter_reference_t<InputIt>>
      && std::assignable_from<
           T&, std::invoke_result_t<
                 std::plus<>&, T, std::iter_reference_t<InputIt>>>
T my_accumulate(InputIt first, InputIt last, T value) {
  return my_accumulate(first, last, std::move(value), std::plus<>{});
}

Run It

 1#include <concepts>
 2#include <cstddef>
 3#include <functional>
 4#include <iostream>
 5#include <iterator>
 6#include <type_traits>
 7#include <utility>
 8#include <vector>
 9
10// using
11template <std::input_iterator InputIt, class T, class BinaryOp>
12requires std::movable<T>
13      && std::invocable<BinaryOp&, T, std::iter_reference_t<InputIt>>
14      && std::assignable_from<
15           T&, std::invoke_result_t<
16                 BinaryOp&, T, std::iter_reference_t<InputIt>>>
17T my_accumulate(InputIt first,
18                InputIt last,
19                T value,
20                BinaryOp op) {
21  while (first != last) {
22    value = std::invoke(op, std::move(value), *first);
23    ++first;
24  }
25  return value;
26}
27
28// default operation
29template <std::input_iterator InputIt, class T>
30requires std::movable<T>
31      && std::invocable<
32           std::plus<>&, T, std::iter_reference_t<InputIt>>
33      && std::assignable_from<
34           T&, std::invoke_result_t<
35                 std::plus<>&, T, std::iter_reference_t<InputIt>>>
36T my_accumulate(InputIt first, InputIt last, T value) {
37  return my_accumulate(first, last, std::move(value), std::plus<>{});
38}
39
40int main() {
41  std::size_t sum = 0;
42  std::vector<std::size_t> values = {1, 1, 2, 3, 5, 8, 13, 21, 34};
43  sum = my_accumulate(values.begin(), values.end(), sum);
44  std::cout << "vector sum = " << sum << '\n';
45
46  std::size_t product = 1;
47  product = my_accumulate(values.begin(), values.end(), product,
48                          std::multiplies<std::size_t>{});
49  std::cout << "vector product = " << product << '\n';
50}

Note that we did not pass + or * to a function. The symbol + is not a type.

The template parameter BinaryOp names the type of the callable object, and the parameter op is an object of that type. The object must satisfy the C++20 std::invocable concept for the accumulator and element types.

Lambda expressions, function objects, and function pointers are all acceptable callables. The standard library provides a large collection of function objects such as std::plus and std::multiplies.