Not all choices can be made at compile-time, sometimes including which function (of several possibilities) to invoke. This article aims to cover all of the methods available to Modern C++ when selecting which function to call, with the decision made at runtime (based upon user input, for example). Terms covered in this article include function pointers, function objects, virtual functions and lambdas.
1. Control flow and (global) functions
Sometimes simplest is best. Consider the following C++ code fragment (the only thing which makes it non-C is use of std::string):
int f(double, double); // function declarations only
int g(std::string); // (possibly in a header file)
void use_result(bool use_numbers) { // client function
int result;
if (use_numbers) {
result = f(2.5, 3.0);
}
else {
result = g("Hello");
}
// use variable "result"
}
Here we are assigning the int return value from either function f() or g() to the variable result, depending upon the value of Boolean variable use_numbers. In the case that these two (or more) functions have different signatures, this is the only option available. Old-skool and crude maybe, and with more than a couple of functions to choose between, it becomes error-prone to list (so possibly a switch statement may be a more appropriate tool). Note that for this method, all functions which make up the selection must have the same return type, or one convertible to the same type.
2. Function pointers
Being able to assign a pointer-to-function to an appropriately-typed pointer variable is possible in C as well as C++. This comparatively cheap addition to the language (at the expense of obtuse syntax for declarations) gives it a level of polymorphic capability.
Assuming we want to use (a) function(s) which take two doubles and return an int, we can use either C or Modern C++ syntax to declare the variable:
#include <iostream>
int floor_divide(double a, double b) {
return static_cast<int>(a / b);
}
int main() {
int(*pf1)(double, double) = &floor_divide; // C-style function pointer
using PFType = int(*)(double, double); // Modern C++ using-declaration
PFType pf2 = &floor_divide;
std::cout << "floor(7.0 / 3.3) = " << (*pf1)(7.0, 3.3) << '\n';
std::cout << "floor(7.0 / 3.3) = " << (*pf2)(7.0, 3.3) << '\n';
}
In this code, typed pointer pf1 is defined and assigned to on the same line, while identically-typed pointer pf2 is defined and assigned from the already-declared PFType. Note that the binding of the dereferencing * with the function-pointer variable name requires parentheses to protect it from usual precedence rules, as in (*pf1) and (*pf2).
3. Virtual Function Call
Class hierarchies allow the use of virtual function definitions and function call(s) on a base class pointer (or reference). Consider the following hierarchy of an ABC (“Abstract Base Class”, having a single pure-virtual member function) and three derived classes:
#include <iostream>
#include <cmath>
class DivideToInt {
protected:
double divisor, dividend;
public:
DivideToInt(double divisor, double dividend) : divisor{ divisor }, dividend{ dividend } {}
virtual int div() const = 0;
virtual ~DivideToInt() {}
};
class CeilingDivide : public DivideToInt {
public:
using DivideToInt::DivideToInt;
virtual int div() const override {
return static_cast<int>(ceil(divisor / dividend));
}
};
class FloorDivide : public DivideToInt {
public:
using DivideToInt::DivideToInt;
virtual int div() const override {
return static_cast<int>(floor(divisor / dividend));
}
};
class NearestDivide : public DivideToInt {
public:
using DivideToInt::DivideToInt;
virtual int div() const override {
return static_cast<int>(round(divisor / dividend));
}
};
int main() {
DivideToInt *divisions[3];
divisions[0] = new CeilingDivide(7.0, 3.3);
divisions[1] = new FloorDivide(7.0, 3.3);
divisions[2] = new NearestDivide(7.0, 3.3);
std::cout << "ceil(7.0 / 3.3) = " << divisions[0]->div() << '\n';
std::cout << "floor(7.0 / 3.3) = " << divisions[1]->div() << '\n';
std::cout << "round(7.0 / 3.3) = " << divisions[2]->div() << '\n';
for (auto& d : divisions) {
delete d;
d = nullptr;
}
}
In the main() function above, use of the class DivideToInt has two stages, these being object construction and invocation of its pure-virtual function div(). The three derived classes inherit publicly from this class, so pointers to them (obtained by using new and their class constructors) can be stored in an array of pointers to DivideToInt objects. The operation divisions[n]->div() is evaluated at runtime to choose between one of CeilingDivide::div, FloorDivide::div and NearestDivide::div. To avoid memory leaks, the clean-up code iterates through the array by reference, so that setting the range-for loop variable to nullptr zeroizes the array element itself, not a copy.
4. Function Objects (Functors)
An alternative to overloading a base-class’s pure-virtual function is to overload operator() (the call operator) in a function object. The following code chooses which function to invoke based upon use of a dummy (strongly-typed) parameter, this method of function selection is known as tag-dispatch:
#include <iostream>
#include <cmath>
class DivideCeiling {};
class DivideFloor {};
class DivideNearest {};
class DivideToInt {
double divisor, dividend;
public:
DivideToInt(double divisor, double dividend)
: divisor{ divisor }, dividend{ dividend } {}
int operator()(const DivideCeiling&) {
return ceil(divisor / dividend);
}
int operator()(const DivideFloor&) {
return floor(divisor / dividend);
}
int operator()(const DivideNearest&) {
return round(divisor / dividend);
}
};
int main() {
DivideToInt division(7.0, 3.3);
std::cout << "ceil(7.0 / 3.3) = " << division(DivideCeiling{}) << '\n';
std::cout << "floor(7.0 / 3.3) = " << division(DivideFloor{}) << '\n';
std::cout << "round(7.0 / 3.3) = " << division(DivideNearest{}) << '\n';
}
Notice again that use of the DivideToInt class has two parts, construction and usage. The parameter to the operator() call is an object of type matched by exactly one of the overloads in the class definition.
5. Lambdas and std::function
Lambdas are usually defined with the auto keyword, because they have complex signatures (based on the function in which they are defined and the capturing mode) which cannot be represented in C-style function pointers. The good news is that std::function can be used to define an empty variable which can have a lambda assigned to it at some later point. The following program’s main() function assigns to such a variable (called df) inside a switch statement:
#include <functional>
#include <iostream>
#include <cmath>
#include <initializer_list>
enum class DivisionPolicy { Ceiling, Floor, Nearest };
int main() {
std::function<int(double,double)> df;
for (auto p : { DivisionPolicy::Ceiling, DivisionPolicy::Floor, DivisionPolicy::Nearest }) {
switch (p) {
case DivisionPolicy::Ceiling:
df = [](double a, double b) -> int { return ceil(a / b); };
break;
case DivisionPolicy::Floor:
df = [](double a, double b) -> int { return floor(a / b); };
break;
case DivisionPolicy::Nearest:
df = [](double a, double b) -> int { return round(a / b); };
break;
}
std::cout << static_cast<int>(p) << ": " << df(7.0, 3.3) << '\n';
}
}
In this code again use of variable df comprises two parts, creation from one of three lambda definitions, and then invocation with familiar function-call syntax. Notice that even though df is defined without assignment outside of the loop, it can be assigned from any lambda declaration with the correct signature (in this case using trailing return type -> int). This is the most modern-feeling of the methods covered so far, but is in fact valid C++11 code.
6. Dictionary Lookup
Rather than store function pointers in an array as for method 3, we can make use of a std::unordered_map with keys of type std::string and values of type std::function. The following code demonstrates use of this:
#include <functional>
#include <string>
#include <unordered_map>
#include <iostream>
#include <cmath>
#include <initializer_list>
std::unordered_map<std::string, std::function<int(double,double)>> DivisionPolicy = {
{ "ceiling", [](double a, double b) -> int { return ceil(a / b); } },
{ "floor", [](double a, double b) -> int { return floor(a / b); } },
{ "nearest", [](double a, double b) -> int { return round(a / b); } }
};
int main() {
for (auto p : { "ceiling", "floor", "nearest" }) {
std::cout << p << ": " << DivisionPolicy[p](7.0, 3.3) << '\n';
}
}
Note that this code is the briefest we have seen so far, and builds upon concepts seen in method 5, above. The use of a global std::unordered_map called DivisionPolicy minimizes heap allocation time during the run of the program and allows it to be visible to all functions. (An alternative could be passing a reference to a std::unordered_map to all client functions.)
The use of DivisionPolicy[p](7.0, 3.3) does risk an exception being thrown if an invalid value for p is used; production code should probably make use of a custom function that calls DivisionPolicy.find(), where the returned std::pair has the required lambda as the field called second. The separation between creation of the lambdas and use of the desired one, is clearer than for any of the previous methods shown. Ease and guaranteed-correctness of extensibility is enhanced, and it could be argued that this method provides a level of reflection to the language, in addition to polymorphism.
That wraps things up for this article on choosing between different functions at runtime. You should now be able to make an informed decision as to which of these methods to use in your code (or even whether compile-time selection would be more appropriate), in order to make it more easily extensible and less bug-prone. Source code for this article is available on GitHub.