Strategy and policy

A strategy is the part of an algorithm that varies: the comparison of a sort, the operation of a calculator, the step of a compression. The pattern is the decision of when that part is bound, at run time or at compile time.

Run time costs a call through a std::function or a function pointer and allows the choice to depend on a config file. Compile time costs nothing and fixes the choice in the type. Which one is right depends on who chooses, and this lesson shows both with the same problem.

The two designs also differ in how a test replaces them: a policy passed as an argument is swapped for a counting stub, and a policy fixed in the type is swapped by instantiating the test with another type. Both are simple, and both are visible in the signature.

A strategy at run time

std::function<bool(int, int)> holds any comparison with that signature. The sort takes it as its third argument, so the same vector is sorted ascending and then descending without changing its type or the call.

The program prints 1 2 3 and then 3 2 1. The strategy is one variable of the caller, and a configuration value could decide which assignment it gets.

The price is one indirect call per comparison, which is measurable on a large sort. Lesson 125 explained the mechanism behind std::function; here it is used for its intended purpose, the replaceable algorithm.

#include <algorithm> #include <functional> #include <iostream> #include <vector> void print(const std::vector<int>& values) { for (int value : values) { std::cout << value << ' '; } std::cout << '\n'; } int main() { std::vector<int> values{3, 1, 2}; std::function<bool(int, int)> order = std::less<int>{}; std::sort(values.begin(), values.end(), order); print(values); order = std::greater<int>{}; std::sort(values.begin(), values.end(), order); print(values); }

g++ -std=c++23 -Wall -Wextra runtime.cpp -o runtime_strategy
./runtime_strategy

1 2 3
3 2 1

A policy at compile time

A template parameter is the other way to choose a strategy. sort_with<Compare> takes the comparison as a type and calls it directly, so the compiler sees the exact comparison and can inline it.

The program sorts with std::greater<int> and prints 5 4 2. The call has no indirection, and the two instantiations of the function are two different functions in the binary.

The cost is that the choice is part of the type: a configuration value cannot change it at run time, and each policy instantiation is compiled separately. The standard library uses this trade in std::sort itself, where the comparison is a template parameter with a default.

#include <algorithm> #include <functional> #include <iostream> #include <vector> template <typename Compare> void sort_with(std::vector<int>& values, Compare compare) { std::sort(values.begin(), values.end(), compare); } int main() { std::vector<int> values{2, 5, 4}; sort_with(values, std::greater<int>{}); for (int value : values) { std::cout << value << ' '; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra compile.cpp -o compile_policy
./compile_policy

5 4 2

A function pointer is enough

When the strategy is a plain function with a known signature, a function pointer is the smallest tool: no allocation, no std::function object, and the assignment is a pointer store.

The program's calculator starts with addition and is switched to multiplication by one assignment. The two results are 5 and 6, and the class holds a pointer and nothing else.

A function pointer cannot capture state, which is the limit that sends a design to std::function. For a table of named operations, the pointer or a small struct with a name is the better choice: it is one machine word and it can be compared and stored.

#include <iostream> int add(int a, int b) { return a + b; } int multiply(int a, int b) { return a * b; } class calculator { public: explicit calculator(int (*operation)(int, int)) : operation_(operation) {} int run(int a, int b) const { return operation_(a, b); } void use(int (*operation)(int, int)) { operation_ = operation; } private: int (*operation_)(int, int); }; int main() { calculator calc{add}; std::cout << calc.run(2, 3) << ' '; calc.use(multiply); std::cout << calc.run(2, 3) << '\n'; }

g++ -std=c++23 -Wall -Wextra pointer.cpp -o pointer_strategy
./pointer_strategy

5 6

The policy that cannot be called

A template accepts any type, and the error appears where the type is used as a function. The program passes a struct without operator() to a function that calls its policy, and the compiler reports that no call matches.

The message names the type and the argument list, and it points at the call inside the template rather than at the instantiation. The fix is the missing operator, or a lambda with the right signature.

A concept makes the requirement explicit: requires requires(Policy p, int v) { p(v); } rejects the type at the call site with a message about the constraint. Since C++20 that is the clearer way to say what a policy has to provide.

struct no_call { }; template <typename Policy> int apply(Policy policy, int value) { return policy(value); } int main() { return apply(no_call{}, 1); }

g++ -std=c++23 -Wall -Wextra no_call.cpp -o no_call

no_call.cpp: In instantiation of 'int apply(Policy, int) [with Policy = no_call]':
no_call.cpp:13:17: required from here
13 | return apply(no_call{}, 1);
| ~~~~~^~~~~~~~~~~~~~
no_call.cpp:8:18: error: no match for call to '(no_call) (int&)'
8 | return policy(value);
| ~~~~~~^~~~~~~

Exercises

  1. Add a third comparison to the runtime program and print the vector sorted by absolute value.
  2. Turn the calculator into a std::function member and add an operation that captures a multiplier.
  3. Write a concept that requires a policy to be callable with an int and use it on apply.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What decides the strategy when it is a std::function member?

    Answer

    The value assigned at run time

    The function object stores whatever callable was assigned, so the choice can follow a configuration value.

  2. What is the cost of a strategy chosen at compile time?

    Answer

    The choice is fixed in the type and cannot change at run time

    A template argument is part of the type, and each instantiation is a separate function.

  3. When is a function pointer the right strategy?

    Answer

    When the operation is a plain function with a fixed signature

    A pointer has no storage for state, which is exactly what a free function does not need.

  4. What does this program print? #include <algorithm> #include <functional> #include <iostream> #include <vector> void print(const std::vector<int>& values) { for (int v : values) std::cout << v << ' '; } int main() { std::vector<int> values{3, 1, 2}; std::function<bool(int, int)> order = std::less<int>{}; std::sort(values.begin(), values.end(), order); print(values); std::cout << " / "; order = std::greater<int>{}; std::sort(values.begin(), values.end(), order); print(values); }

    Answer

    1 2 3 / 3 2 1

    The first sort uses less and the second greater, and both print the whole vector.

  5. What does this program print? #include <algorithm> #include <functional> #include <iostream> #include <vector> template <typename Compare> void sort_with(std::vector<int>& values, Compare compare) { std::sort(values.begin(), values.end(), compare); } int main() { std::vector<int> values{2, 5, 4}; sort_with(values, std::greater<int>{}); for (int value : values) std::cout << value << ' '; }

    Answer

    5 4 2

    The comparison is std::greater, so the three values come out in descending order.

  6. What does this program print? #include <iostream> int add(int a, int b) { return a + b; } int multiply(int a, int b) { return a * b; } class calculator { public: explicit calculator(int (*operation)(int, int)) : operation_(operation) {} int run(int a, int b) const { return operation_(a, b); } void use(int (*operation)(int, int)) { operation_ = operation; } private: int (*operation_)(int, int); }; int main() { calculator calc{add}; std::cout << calc.run(2, 3) << ' '; calc.use(multiply); std::cout << calc.run(2, 3); }

    Answer

    5 6

    It adds 2 and 3, then switches to multiply and prints 6.

  7. Which strategy can capture a local variable?

    Answer

    A lambda stored in a std::function

    The capture lives in the function object; a pointer has nowhere to keep it.

  8. What does the compiler report for a policy without operator()?

    Answer

    That no call matches the type and the argument list

    The error appears at the call inside the template, and the message names the type and the arguments.

  9. Why is std::sort's comparison a template parameter rather than a std::function?

    Answer

    So the comparison is inlined and no indirect call is made

    A sort compares many times, and the direct call lets the compiler inline the comparison.

  10. How do you state the requirement of a policy in C++20?

    Answer

    With a concept over the policy type

    A concept checks the expression at the call site and reports the unmet requirement there.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev