0% found this document useful (0 votes)
5 views76 pages

Generic Programming in C++ for Filters

The document discusses the challenges and solutions of implementing generic programming in the context of numerical integration and filter evaluation for complex engineering problems. It emphasizes the need for efficient data structures and runtime polymorphism to handle potentially millions of filters and equations. The document also highlights the importance of creating scalable and flexible algorithms that can adapt to various functions and integration methods.

Uploaded by

007jiegao
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views76 pages

Generic Programming in C++ for Filters

The document discusses the challenges and solutions of implementing generic programming in the context of numerical integration and filter evaluation for complex engineering problems. It emphasizes the need for efficient data structures and runtime polymorphism to handle potentially millions of filters and equations. The document also highlights the importance of creating scalable and flexible algorithms that can adapt to various functions and integration methods.

Uploaded by

007jiegao
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

GENERIC PROGRAMMING

EEEE3084 – Scalable, Cross-


Platform Software Design
EXAMPLE PROBLEM
Where might what we are going to look at be useful?
HYPOTHETICAL PROBLEM
• Computers are used to solve mathematical problems
• What happens if lightening hits an aircraft?
• An electromagnetic problem, explained by Maxwell’s equation

• Maxwell’s Equations:
• Partial differential equations, very difficult to solve directly
• Solved by numerical approaches

• Numerical approaches
• Convert problems described by complex differential equations into ‘lots of’
smaller but more simple problems.
• ‘lots of’ can mean millions of much simpler equations that must be solved in
parallel
HYPOTHETICAL PROBLEM
• Aircraft ‘skin’ modelled by considering its surfaces
• Many panels used to describe each surface
• The penetration of fields through the skin is modelled by equations that link surface
polygons

• If each panel linked to all other panel:


• 1000 panels means 1,000,000 equations
• Each equation:
• Does the same job
• But will be slightly different depending on relative size/location/alignment of panels

• Think of the equations as filters:


• Describe how fields hitting one panel emerge from another
• Each filter is different, so need a generic way of describing a filter that can apply to any
panel combination
HYPOTHETICAL PROBLEM
• Each panel needs a ‘filter’ that links it to each other polygon (and possibly itself)
• Here there are 8 panel, gives 64 filters
• 1000 panel gives 1,000,000 filters (1000 is still not a lot for this type of problem, could be millions)
• 1,000,000 panels, gives 1,000,000,000,000 filters (1 million panels in an engineering problem is not uncommon)

• Each filter:
• Needs to know which panel it connects, i.e. its input and output
• Contain the transfer function / equation that links these
• Probably represented by some sort of data structure or class, almost certainly then have a large array of filters

• Perspective: 1,000,000,000,000 filters


• If each filter takes 1us to evaluate, the total time required is 1,000,000,000,000 x 10-6 = 1 million seconds or
278 hours
• If each filter takes 100 bytes to store, the total storage is 1,000,000,000,000 x 100 B = ~ 90TB

• The point:
• Engineering problems get very large, very quickly.
• Clever ways of storing and processing very large sets of objects / arrays / matrices are needed
FILTER IMPLEMENTATION
• How to store?
• Data structures / Classes
• Representing links – filter has polygon 1 as input and 23012 as output, represent using ID, pointers, etc
• Need to minimise storage requirement due to number of instances

• Equation implementation:
• How is action of each filter stored & evaluated – class member functions, etc
• Many different types of filter, how to reuse filter definition code but change ‘action’ code without rewriting
• Future proof for future modifications – actions required may evolve

• Computational efficiency
• How to efficiently evaluate all filters – important due to number of instances
• Efficient implementation – get most from each core
• Parallelisation – make sure all available cores are used

• Variable types
• Might sound trivial but…
• 32 or 64 bit integers, signed or unsigned – 64 bit required if you want to index more than 2^32 filters but will double storage requirements
• Similar situation with floats vs doubles
HYPOTHETICAL SOLUTION class Panel;
class Filter;

main() {
• Use C++ as OOP solves some of the problems
• E.g. can have different types of filter with polymorphism and inheritance int numPanels = X;
int numFilters = Y;
int numTimeSteps = Z;

• Filter class would be an abstract class with a virtual doFiltering() member Panel* panels[numPanel];
Filter* filters[numFilters];
function
// Read patches / filters from file

for( int i = 0; i < numTimeSteps; i++ ) {


• Specific instances of Filter would derive/inherit from this for( int j = 0; j < numFilters; j++ ) {
filters[j]->evaluate();
• Each implementing its own evaluate }
• Each filter type specified when created }
}

• Run-time polymorphism allows C++ to determine which type of filter it is dealing


with
• And therefore which evaluate implementation to use
REMINDER
class Filter {
public:
void evaluate();
};

• Abstraction, inheritance and virtual functions class Filter1 : public Filter {


public:
void evaluate() {
• A function in a class which has no definition is a virtual function // filter code for filter type 1
}
• Sub-classes must define it };

• In this case, the virtual function in Filter means that all filters must have a class Filter2 : public Filter {
doFiltering() function, but this function is not defined for the generic filter public:
void evaluate() {
case // filter code for filter type 2
}
};

• Filter1 and Filter2 each have evaluate() functions which are different
• Example of polymorphism main() {
Filter *filters[2];

filters[0] = new Filter1();


• When and array of filters is made, and doFiltering() called on each of these filters[1] = new Filter2();
filters, a decision must be made about which instance of doFiltering to use.
filters[0]->evaluate(); // calls evaluate in Filter1
filters[1]->evaluate(); // calls evalaute in Filter2
}
• When is this decision made?
HYPOTHETICAL SOLUTION class Panel;
class Filter;

main() {
• Back to the original example
int numPanels = X;
int numFilters = Y;
• The array of Filters int numTimeSteps = Z;
• Size is potentially unknown when the program is compiled – Y is probably read from an input file
• The type of each filter is also unknown, as this will be read from the input file Panel* panels[numPanels];
Filter* filters[numFilters];

• The compiler cannot decide which version of evaluate is used // Read patches / filters from file
• It isnt known until the program runs and the input file read
for( int i = 0; i < numTimeSteps; i++ ) {
for( int j = 0; j < numFilters; j++ ) {
• A decision must be made when the program runs – at runtime filters[j]->evaluate();
}
• Known as runtime-polymorphism }
• Effectively using pointers to functions to select which function runs }

• This can have some implications:


• Can be slow
• Can also give rise to cybersecurity issues – see other module!

• Is this an issue here? Are there alternatives?


RUNTIME POLYMORPHISM – A TANGENT
main() { class Filter { main() {
double total = 0.; protected: Filter* f[2];
double d1; double total = 0.;
srand(time(NULL)); double d2;
srand(time(NULL));
Filter1 f[2] = { Filter1( rand()%10, rand()%10 ), public:
Filter1( rand()%10, rand()%10 ) Filter() : d1(0), d2(0) {} for( int i = 0; i < 2; i++ ) {
}; Filter( double _d1, double _d2 ) : d1(_d1), d2(_d2) {}; if( rand()%2 ) {
virtual double evaluate( double x ) = 0; f[i] = new Filter1( (rand()%10),(rand()%10) );
for( int i = 0; i < 2; i++ ) { }; cout << "Filter is type 1" << endl;
cout << "Filter is type 1" << endl; } else {
} class Filter1 : public Filter { f[i] = new Filter2( rand()%10, rand()%10 );
cout << "Filter is type 2" << endl;
for( int i = 0; i < 2; i++ ) { public: }
total += f[i].evaluate( rand()%5 ); Filter1( double _d1, double _d2 ) : Filter( _d1, _d2 ) {} }
}
double evaluate( double x ) { for( int i = 0; i < 2; i++ ) {
cout << "total: " << total << endl; return d1*x + d2*x*x; total += f[i]->evaluate( rand()%5 );
} } }
};

Non-polymorphic code – just class Filter2 : public Filter { }


cout << "total: " << total << endl;

create a few instances of public:

Filter1. Filter2( double _d1, double _d2 ) : Filter( _d1, _d2 ) {} Polymorphic code – use
double evaluate( double x ) {
return d1*x*x + d2*x*x*x; random numbers to decide
}
}; whether filters should be type
1 or type 2 and store general
Example Filter classes Filter pointers to new objects.
[Link] [Link]
GHIDRA DECOMPILATION
• Compile both with g++, and decompile with Ghidra

• Would expect that Polymorphic code is more complicated and slower – it has to decide which variant of class it is dealing with when evaluate
is called.

• In fact, very little difference. Polymorphic uses function pointers in class array, adds a small offset to the address of each Filter to get the
address of the evaluate function. Filters just lined in in Filter array – doesn’t really make much difference to code

• The code here is not optimised – the compiler basically uses my code as a template for the machine code. It replicates my C++ in machine
code, structures, loops, etc

• If you compare the two programs by looking at disassembly and decompiled code, there is very little difference. Both “evaluate” loops take
similar number of assembly instructions (105 for MM and 110 for PM) and the decompiled code is similar for both
• Small amount of extra code for PM, but not really significant
[Link]
[Link]
USE OF OPTIMISER
• A second version of the program is compiled, this time allowing the compiler to optimise my code (g++ -O3).

• This time the decompiled code is quite different


• For the PM code – the main difference is that the optimiser combines by two separate loops into one (new Filter, evaluate). Other than that, the
code is very similar.
• The MM code looks very different, one interesting aspect is if I look at the “Symbol Tree” (basically a variable/function listing). Under classes, there
are no Filters. For the PM code both Filter1 and Filter2 still exist, but they have been removed in MM.
• The compiler realises that the Filter classes are basically just used for their evaluate functions
• It also realises that the evaluate functions are very simple, the really don’t need to be functions – you could just paste the evaluate equation into the calling
function
[Link]
[Link]
SUMMARY Classes
• Runtime polymorphism is the standard OOP way of having different variants of the same
function
• It works, but it produces code that is difficult / impossible to optimise
Compiler
• Optimisation is the compiler trying to produce code that does the job you requested as
efficiently as possible
• With run-time polymorphism, you don’t completely specify what you want – you leave
options open when the code is compiled Option 1 Option 2
• The final decision isn’t made until the code runs (clue’s in the name, runtime!)
• Means the executable has to contain all options, and additional code to decide which option to
take

• If the decision about which code would be run could be made at compile time, the
compiler/optimiser can only insert the code that is actually used (and no decision making
code
Executable
• Obviously this isn’t always possible (like in our case with random choices), but in many
cases you have flexible classes to allow different actions in different parts of the program
but the decision about which action will be taken can be predetermined
• Some of this is about program design – make it clear what you want
• Some of it is about using OOP features that allow the compiler to more easily optimise…
GENERIC PROGRAMMING
Reduce code, and algorithms, to their most general and useful form.

Don’t write code specific to one application of an algorithm, write code that describes the general
algorithm that can subsequently be applied in any application
A SIMPLIFIED PROBLEM
• Assume the ‘filters’ from the example can contain functions, and that these
functions might need to be integrated.
• Different filters have different functions
• There are different ways of integrating – trading off speed and accuracy

• The challenge becomes: Develop a sub-routine that is a ‘Universal Numerical


Integrator’
• This will take a function and integrate it between two limits
• The function could be anything – different filters will use different functions, many
different filters might be used.
• The way the function is integrated may need to change – to make the process faster
or more accurate
• The integrator should be scalable – new functions or integration methods may be
added at any time
REMINDER – NUMERICAL INTEGRATION
𝑏
• Multiple evaluations of function
𝐼 = න 𝑓(𝑥) 𝑑𝑥
𝑎
• Simple approximation is:
𝐼 = 0.5 ∗ b − a × 𝑓(a) × 𝑓(b)
f(x)

• An integrator function needs to be able to


evaluate the function, and know values for a
and b. x
a b
IMPLEMENTATION AS A FUNCTION
𝑏

Function to
𝐼 = න 𝑓(𝑥) 𝑑𝑥
integrate 𝑎

Numerical
Lower Limit Integrator Function ()
Answer

Upper Limit

Arguments Function Return value

Integrator function should be able to integrate any function, between any limits
INTEGRATOR FUNCTION DEFINITION 𝑏

• What might the function definition look like?


𝐼 = න 𝑓(𝑥) 𝑑𝑥
𝑎
• Return value:
double integrate( ? ); // return a double
• float or double?
float integrate( ? ); // return a float

• Arguments: need to pass the function to


integrate. and the limits:
• How to represent a function? double integrate( Function f, double a, double b );
• Is this a class? Pass the class, pass a pointer to the
class? float integrate( Function f, float a, float b );
• Is it something else?
double integrate( Function *f, double a, double b );
• Arguments could be floats or doubles?
float integrate( Function *f, float a, float b );
FUNCTION TO INTEGRATE - A POOR SOLUTION
𝑏
• Need to integrate different functions
𝐼 = න 𝑓(𝑥) 𝑑𝑥
• Could have different versions of the integrate function 𝑎

• Problems:
double integrate_functionA( double a, double b );
• You create a copy of the function for every different double integrate_functionB( double a, double b );
integration routine double integrate_functionC( double a, double b );

• If you then want to implement two different integration types if( [Link] == FTYPE_A ) {
(1st order, 2nd order, etc), you need two sets of integration I = integrate_functionA( a, b );
functions 1st order A,B,C,… 2nd order A,B,C,… } else if( [Link] == FTYPE_B ) {
I = integrate_functionB( a, b );
• Quickly becomes unmanageable } else if( [Link] == FTYPE_C ) {
I = integrate_functionC( a, b );
• Makes updating code difficult – update/fix might need to be }
applied across many functions
• Possibly messy code, you might need lots of if/switch
statements
FUNCTION TO INTEGRATE – C-SOLUTION
𝑏
• Having a function that does some subprocess which
varies isn’t an uncommon scenario 𝐼 = න 𝑓(𝑥) 𝑑𝑥
𝑎
• Dealt with in C using callbacks / function pointers
• One integrate() can integrate any function double functionA( double x ) {
return 3.*x + 1.;
• The function to integrate is passed to integrate() as a }
pointer – its the memory address
double functionB( double x ) {
return x*x + 4.;
• Now means can add as many additional functions as }
want, without needing to write a new integrate function
double integrate( double (*fptr)(double), double a, double b ) {
return 0.5*(b-a)*fptr(a)*fptr(b);
}
• Can also add new integrate() variants and these
integrate( &functionA, 1. 5. );
should! be able to integrate any of existing functions integrate( &functionB, 1. 5. );
INTEGRATING FUNCTIONS WITH ARGUMENTS
𝑏
• 𝑓 𝑥 = 𝑝1 𝑥 2 + 𝑝2 𝑥 + 𝑝3
𝐼 = න 𝑓(𝑥) 𝑑𝑥
• What if the function takes other parameters – e.g. 𝑝1 ,
𝑎
𝑝2 and 𝑝3 in this case?
double functionC( double x, double p1, double p2, double p3 ) {
return p1*x*x + p2*x + p3;
}
• The previous function pointer method doesn’t work:
• Function pointer definition is: double (*fptr)(double)
• Is restricted to a function with one double argument and
returning a double

double integrate( double (*fptr)(double), double a, double b ) {


• How to deal with this in C? Need a way of defining return 0.5*(b-a)*fptr(a)*fptr(b);
functions with unknown / variable numbers of }
arguments
INTEGRATING FUNCTIONS WITH ARGUMENTS
𝑏
• Computers can deal with this easily at processor level – as long as the calling code and function code both agree/know how many
arguments have been passed each time its called
• The “agree” bit is key – this is why variable argument lists are generally not used at high level
𝐼 = න 𝑓(𝑥) 𝑑𝑥


Much better if it is clear in the source code how many arguments, and of what type, each function takes
Compiler can catch any mistakes which stops bugs getting into program
𝑎
There are ways to do it…

• Force it manually
• Most general way is to have your functions take an array of void pointers as their only argument.
• Each void pointer can be typecast to the correct pointer type
• The cast pointer is then used to retrieve that argument

• Use of variadic functions int printf ( const char * format, ... );


• This is how functions like printf work – remember printf can take any number of arguments, that can be any type
• [Link]

• Becomes very easy to introduce bugs


• The whole reason for function prototypes is that each function is well defined – if you try to pass the wrong argument type, or wrong number
of arguments, the compiler will tell you before the issue goes any further
• If function arguments can be anything, the function must then check to see if its arguments are what it thought they should be
• If you don’t check, and lots of programmers won’t check properly because that takes effort, you introduce bugs and potential cybersecurity
issues.
VARIADIC FUNCTIONS AND ISSUES
• Normal function call
• Calling function pushes arguments onto stack (or into
registers)
• Function called, function retrieves arguments

• Important to know:
• How many arguments (how many values to retrieve)
• What type – this affects size of arguments, and therefore
where each argument in in memory

[Link]
A BASIC C++ APPROACH
class Function {
• OOP approach is to create a function object public:
double evaluate( double x );
• Object has member functions, can use polymorphism
double evaluate( double x, double p1, double p2, double p3 );
• Multiple evaluate functions with different arguments
float evaluate( float x );
float evaluate( float x, float p1, float p2, float p3 );
• Might be a better approach, but each “evaluate” function needs to be };
written – copy/paste, etc

• Doesn’t solve the problem at the integrate function side


• Integrate might needs to force call to correct double/float etc according to
requirements class Function {
public:
• If/else/typecast statements in integrate, or multiple integrate functions
anyType evaluate( anyType x, … );
};
• Ideal scenario is to just write one evaluate function:
• Each class can implement it as it see’s fit – different numbers of parameters,
etc
• Avoid need to rewrite for trivial issues such as small differences in variable
types
A BASIC C++ APPROACH class Function {
public:
double evaluate( double x ){};
};
• Inheritance can be used to reduce evaluate
function down to single definition class Function1 : public Function {
public:
double evaluate( T x ) {
return 3*x;
• Parameters assigned to function when it is };
created };

class Function2 : public Function {


• Evaluate function called to get function value private:
double p1, p2, p3;
at specific x public:
Function2( double _p1, double _p2, double _p3 )
: p1(_p1_), p2(_p2), p3(_p3);

• Doesn’t solve the double/float issue… double evaluate( T x ) {


return p1*x*x + p2*x + p3;
• Still need an instance of each function for };
different types };
CONSTRUCTOR SYNTAX class Function {
public:
double evaluate( double x ){};
};
• Quick point, the classic constructor syntax is:
class Function2 : public Function { class Function1 : public Function {
private: public:
double p1, p2, p3; double evaluate( T x ) {
public:
Function2( double _p1, double _p2, double _p3 ) { return 3*x;
p1 = _p1, p2 = _p2; p3 = _p3; };
} };
};

class Function2 : public Function {


• The syntax to the right is “modern” C++ private:
double p1, p2, p3;
• Allows member variables to be initialised when public:
they are created Function2( double _p1, double _p2, double _p3 )
: p1(_p1_), p2(_p2), p3(_p3);
• Rather than created (with default constructor),
and then initialise with values double evaluate( T x ) {
return p1*x*x + p2*x + p3;
• Potentially saves unnecessary applications };
};
GENERIC PROGRAMMING
• This section is about generic programming
• The solution to previous problem:
• A way to create functions that do certain things
• Without needed to explicitly define the details:
• Arguments
• Variable types
• Etc

• What the program does is clear, how it does it is flexible Worth pointing out that C++ is an evolving
language, template meta-progamming and STL
(two key topics for us) has been around since ’98
• Allows code that can be modified, extended, scaled later but certain other features were only added more
recently

• There are features of OOP languages that can help, that’s what we
are looking at

Image from: [Link]


dispatch-table/
INTRODUCING TEMPLATES
• Taking one part of the problem at a time – how do you deal with different variable types:
• Use of templates – write what the function does, without specifying the variables used. float evaluate( float x ) {
return (x < 0.0f)? 0.0f:1.0f;
}
• Example: say the function is a step function double evaluate( double x ) {
return (x < 0.)? 0.:1.;
• 𝑓 𝑥 =0 𝑥<0 }
• 𝑓 𝑥 =1 𝑥≥0
int evaluate( int x ) {
return (x < 0)? 0:1;
}

• Having a function that returns correct type / takes correct argument can be important – affects
variable size in memory (see later) and result precision
• End up having to rewrite same function multiple times, if you think of all different types of int
(int32_t, int64_t, uint32_t, uint64_t, etc) this can add up to a lot of code is some circumstances
• This problem occurs with many standard library functions – min, max, etc, - all need to work
with different types
INTRODUCING TEMPLATES
// what follows contains an undefined type T
• Templates allow you to define the function: template <typename T>
• without specifying variable types // function that uses instances of T
T evaluate( T x ) {
• but allowing the specification of links between different variables, return (x < 0.)? T(0.):T(1.);
i.e. specifying that the arguments and return values are same type }

• You can create a function, or class, with as-yet undefined


variable types template <typename T>
class Function {
• Function/class must be prefixed with private:
template <typename T> T value1;
T value2;

• These are simple examples with one type, which is called T in // Constructor
Function(T val1, T val2) : value1(val1), value2(val2) {}
this case };

• How does it actually work?


• The compiler is just doing a copy/paste when you compile the
code
• It will try to work out which type to substitute for T
template <typename T>
class Function { This line is added before the usual class

INTRODUCING TEMPLATES
private: definition – it just says that a generic
variable, of type T, will be found in the class.
T value1; The compiler then knows it must try to work
// what follows contains an undefined type T
• Templates allow you to define the function: out what T is each time the class is used (it
T •value2;
without specifying variable types
template <typename T>
can be different for each class instance).
// function that uses instances of T
T evaluate( T x ) {
• but allowing the specification of links between different variables, return (x < 0.)? T(0.):T(1.);
i.e. specifying that the arguments and return values are same type }

// Constructor
• You can create a function, or class, with as-yet undefined
Function(T
variable types val1, T val2) : value1(val1), value2(val2) {}
}; • Function/class must be prefixed with
template <typename T>

• These are simple examples with one type, which is called T in


this case Here you can see where the variable type T
• How does it actually work? has been used, in place of the usual
• float/int/etc
The compiler is just doing a copy/paste when you compile the code
• It will try to work out which type to substitute for T
DETERMINING T
template <typename T>
T evaluate( T x ) { // function that uses instances of T
return (x < 0.)? T(0.):T(1.);
}

• Compiler can work out what T needs to be in


many cases int i(1);
float f(3.1f);
• Obvious that i is int and f is float cout << evaluate(i) << " " << evaluate(f) << "\n";

template <typename T>


class Function {
private:
• If it isn’t unambiguous then you need to tell the T value1;
compiler T value2;

• Creating a new instance of this class with the // Constructor


Function();
default constructor doesn’t give enough Function(T val1, T val2) : value1(val1), value2(val2) {}
information to determine the type };

• If you did the 2nd year project, you’ll see this a


Function f; // this cant work, as there isn’t enough
lot when using VTK. // information to determine the type T
Function<int> fi; // T will be an int
Function<float> ff; // T will be a float this tim
ABBREVIATED FUNCTION TEMPLATES
• Just makes code a bit neater for simple cases template <typename T>
// function that uses instances of T
• Rather than define a template type that will be T evaluate( T x ) {
automatically determined later, you just use the return (x < 0.)? T(0.):T(1.);
}
“auto” variable type
• Can be seen as a generic variable type that
can be used anywhere auto evaluate( auto x ) {
return (x < 0.)? 0.:1.;
}
• Word of caution
• C++ is an evolving language, lots of features int main()
get added over time {
int x = 1;
• Various C++ “releases” names according to
There is an irony here, people
year of release – C++11, C++14, C++17,
constwho
auto&like C++/OOP
y = f(1); will say its
int (*p)(int x);
C++20, etc because it makes code easier to read, reuse, etc however the
p = f;
complexity
• Not all of these features are available in all of the C++ language
auto fpis =starting
p; to do the opposite.
versions, not all compilers support all versions,
etc }
The thing with programming languages is that they all have
advantages, disadvantages and applications where they work best.
template <typename T>

PROGRESS…
class Function {
public:
T evaluate( T x ) {
return T(0);
};
• This is what we had before: };

• Function base class


class Function1 : public Function {
• Function 1 & 2 inherit from this public:
double evaluate( T x ) {
return 3*x;
};
};
• What about now?
class Function2 : public Function {
private:
double p1, p2, p3;
public:
Function2( double _p1, double _p2, double _p3 )
: p1(_p1_), p2(_p2), p3(_p3);

double evaluate( T x ) {
return p1*x*x + p2*x + p3;
};
};
PROGRESS…
template <typename T>
class Function {
public:
T evaluate( T x ) {
return T(0);
• This is what we had before: };
• Function base class };
• Function 1 & 2 inherit from this
class Function1 : public Function<double> {
public:
double evaluate( double x ) {
• What about now? return 3.*x;
• Can template the base class to provide a generic }
};
evaluate function
template <typename U>
class Function1 : public Function<U> {
• But how does inheritance work? public:
• Derived classes can specify the template type for U evaluate( U x ) {
the parent class, either by fixing it or by linking it return 3.*x;
}
to another template };
PROGRESS… template <typename T>
class Function {
public:
T evaluate( T x ) {
return T(0);
• This is what we had before: };
};
• Function base class
• Function 1 & 2 inherit from this
template <typename U, typename V>
class Function2 : public Function<U> {
private:
• What about now? V p1, p2, p3;

• Can template the base class to provide a generic public:


Function2( V _p1, V _p2, V _p3 ) : p1(_p1), p2(_p2),
evaluate function p3(_p3){}

U evaluate( U x ) {
return p1*x*x + p2*x + p3;
• But how does inheritance work? }
};
• Derived classes can specify the template type for
the parent class, either by fixing it or by linking it
to another template

Function_1.cpp
WHAT MIGHT THE INTEGRATION CODE LOOK LIKE?
template <typename U>
class Function1 : public Function<U> {
• Integrate function takes a function as an public:
U evaluate( U x ) {
argument return 3.*x;
}
• Templating this allows any function to be };
passed template <typename U, typename V>
• Requirement is just that T must contain an class Function2 : public Function<U> {
private:
evaluate( ) function V p1, p2, p3;

public:
Function2( V _p1, V _p2, V _p3 ) : p1(_p1), p2(_p2),
p3(_p3){}
• Also takes numerical arguments for limits
U evaluate( U x ) {
return p1*x*x + p2*x + p3;
}
};
• Final point on this, is that code can be made to
less messy. template <typename T, typename U>
U integrate( T f, U a, U b ) {
return 0.5*(b-a)*[Link](a)*[Link](b);
}
WHAT MIGHT THE INTEGRATION CODE LOOK LIKE?
𝑏
• Integrate function takes a function as an
argument
𝐼 = න 𝑓(𝑥) 𝑑𝑥
• Templating this allows any function to be 𝑎
passed 𝐼 = 0.5 ∗ b − a × 𝑓(a) × 𝑓(b)
• Requirement is just that T must contain an
evaluate( ) function template <typename T, typename U>
U integrate( T f, U a, U b ) {
return 0.5*(b-a)*[Link](a)*[Link](b);
}
• Also takes numerical arguments for limits

• Final point on this, is that code can be made to template <typename T, typename U>
less messy. U integrate( T f, U a, U b ) {
return 0.5*(b-a)*f(a)*f(b);
}
FUNCTORS
template <typename T>
class Function {
public:
T evaluate( T x ) {
return T(0);

• Short for function object };


};

• Really just means a class overloads the ()


operator, and can therefore be “called” like a
function template <typename T>
class Function {
public:
• Can simplify code appearance T evaluate( T x ) {
return T(0);
};

U operator() ( U x ) {
return evaluate( x );
}
};
template <typename T, typename U>
U integrate( T f, U a, U b ) {
return 0.5*(b-a)*f(a)*f(b);
}
ABSTRACT CLASSES
template <typename T>
class Function {
public:
T evaluate( T x ) {
return T(0);
• Have intentionally left a default evaluate function in the };
parent function class
U operator() ( U x ) {
• This just returns 0 in whatever type the template is using return evaluate( x );
}
};
• Issue with this is that a Function class could be created,
but it wouldn’t do anything
• You may not want to allow someone to create an empty
function
template <typename T>
class Function {
• Can make Function an abstract class public:
• This means it can be used as basis for derived classes but virtual T evaluate( T x ) = 0;
cannot exist itself
T operator() ( T x ) {
• Done by making at least one of its functions virtual – that return evaluate( x );
means say the function exists (declare it), but decline to }
define it. };
• All non-abstract derived classes must implement this function
CODE OPTIMISATION
Optimising for speed
INLINING for( i = 0; i < nFilter; i++ ) {
double output = filters[i].evaluate( input );
}
• Thinking back to original example: a huge array of filters
(millions?)
• Must be a “evaluate” function in the filter classes, similar to
the integrate class Approximate description! (see cybersecurity
• Typical code might involve a loop forcing each to evaluate module for more details) The point is: there are
quite a few additional instructions required to
• What happens when a function is called? setup and close-down the function
• Arguments for function are pushed onto stack
• Function called If the function code is large, this might be ok.
• Various memory allocation steps for new function’s variables (stack
frame creation)
• Function code executes If the function code is a line or two, you can
• Return value stored somewhere more than double the amount of code that
• Function’s stack frame removed must be run.
• Jump back to calling function
• Arguments removed from stack
If this is happening millions of times, it can cost
quite a bit of time!
INLINING for( i = 0; i < nFilter; i++ ) {
double output = filters[i].evaluate( input );
}
• Imagine the filter was just an integrator:
• Here there would be two function calls
• But only one useful line of code: 0.5*(b-a)*f(a)*f(b); template <typename T>
• Potentially quite inefficient class Filter3 : public Filter {
private:
Function1<T> fn;
• Inlining is a method used to address this public:
T evaluate( T t ) {
• Compiler doesn’t insert function call
return integrate( fn, t, t+100. );
• Copies useful code from called function into calling function
}
};

• Inlining influenced by two things:


• Compiler has code optimisation capability, will try
to use it when appropriate template <typename T, typename U>
U integrate( T f, U a, U b ) {
• return 0.5*(b-a)*f(a)*f(b);
}
INLINING for( i = 0; i < nFilter; i++ ) {
double output = filters[i].evaluate( input );
}
• Imagine the filter was just an integrator:
• Here there would be two function calls
• But only one useful line of code: 0.5*(b-a)*f(a)*f(b); template <typename T>
• Potentially quite inefficient class Filter3 : public Filter {
private:
Function1<T> fn;
• Inlining is a method used to address this public:
inline T evaluate( T t ) {
• Compiler doesn’t insert function call
return integrate( fn, t, t+100. );
• Copies useful code from called function into calling function
}
};

• Inlining influenced by two things:


• Compiler has code optimisation capability, will try
to use it when appropriate template <typename T, typename U>
inline U integrate( T f, U a, U b ) {
• You can hint that you think function should be return 0.5*(b-a)*f(a)*f(b);
inlined using inline keyword }
INLINING for( i = 0; i < nFilter; i++ ) {
double output = filters[i].evaluate( input );
}
• Advantages:
• Reduces overhead associated with function calls
template <typename T>
class Filter3 : public Filter {
• Disadvantages: private:
• Increases amount of compiled code (size of .exe) Function1<T> fn;
public:
• Can’t necessarily force inlining inline T evaluate( T t ) {
• If you write code in the wrong way, you can effectively return integrate( fn, t, t+100. );
block it from happening! }
};

• Key point:
• Inlining involves the compiler copying code for a function
into another function template <typename T, typename U>
• The compiler must know which function will be called inline U integrate( T f, U a, U b ) {
• This isn’t always the case! return 0.5*(b-a)*f(a)*f(b);
}
REMINDER…
template <typename T>
class Filter {
public:
virtual T evaluate( T t ) = 0;
};
• Runtime polymorphism
template <typename T>
• Is used to provide flexibility at runtime
class Filter1 : public Filter<T> {
• In this case, when we write the code we know we will have a set of filters, but public:
not the type of each filter. This is because the filter list will be read from a file T evaluate( T t ) {
when the program runs (at runtime). // do some filtering
• Runtime polymorphism allows an array of filter pointers to be defined, but }
each is assigned to a specific filter type object only at runtime. };

• The remaining code (evaluate) is type agnostic – all filters have an evaluate
function, don’t need to worry about the type
class Panel;
class Filter;
• Each filter type has its own evaluate function:
main() {
• The compiler cannot know which function is going to be called in each loop
cycle because it doesn’t know which type of filter it is dealing with
int numPanels = X;
• This must be decided at runtime int numFilters = Y;
int numTimeSteps = Z;

• Implications Panel* panels = new Panel[numPanels];


Filter* filters = new Filter[numFilters];
• Cannot inline function – compiler must copy/paste code, doesn’t know which
code to copy/paste // Read patches / filters from file
• There must be additional hidden code that decides which evaluate() to call,
each time call is made for( int i = 0; i < numTimeSteps; i++ ) {
for( int j = 0; j < numFilters; j++ ) {
filters[j].evaluate();
}
}
}
REMINDER…
template <typename T>
class Filter {
public:
virtual T evaluate( T t ) = 0;
};
• Runtime polymorphism This is a criticism of OOP. You write nice simple code,
• Is used to provide flexibility at runtime then the compilertemplate
adds <typename
a load of T>
extra code in the
class Filter1 : public Filter<T> {
• In this case, when we write the code we know we will have a set of filters, but
not the type of each filter. This is because the filter list will be read from a file
backgroundpublic:
to Tmake the classes work.
evaluate( T t ) {
when the program runs (at runtime). // do some filtering
• Runtime polymorphism allows an array of filter pointers to be defined, but }
each is assigned to a specific filter type object only at runtime. There is };a quote from someone:
• The remaining code (evaluate) is type agnostic – all filters have… Because the problem with object-oriented languages
an evaluate
function, don’t need to worry about the type
is they’ve got all this class
implicit
Panel;environment that they carry

• Each filter type has its own evaluate function:


around with them. You class wanted
Filter; a banana but what you
• The compiler cannot know which function is going to be called in each gotloopwas a gorilla holding
main() { the banana and the entire

intjungle.
cycle because it doesn’t know which type of filter it is dealing with
numPanels = X;
• This must be decided at runtime int numFilters = Y;
int numTimeSteps = Z;

• Implications
It can be possible to avoid
Panel*it,panels
but =you need to be careful.
new Panel[numPanels];
Filter* filters = new Filter[numFilters];
• Cannot inline function – compiler must copy/paste code, doesn’t know which
code to copy/paste // Read patches / filters from file
• There must be additional hidden code that decides which evaluate() to call,
each time call is made for( int i = 0; i < numTimeSteps; i++ ) {
for( int j = 0; j < numFilters; j++ ) {
filters[j].evaluate();
}
}
}
#include <iostream>
#include <iostream>
using namespace std;
using namespace std;
class Function {
class Function {
public:
public:
inline double evaluate( double x ) {
inline double evaluate( double x ) {
return 3*x; Example program return 3*x;
}
}
inline double operator() (double x) {
inline double operator() (double x) {
return evaluate( x );
} Removed templating, }
return evaluate( x );
};
etc for simplicity – just };

want to demonstrate
inline double integrate( Function f, double a, double b ) {
return 0.5*(b-a)*f(a)*f(b); effect of inlining inline double integrate( Function f, double a, double b ) {
return 0.5*(b-a)*f(a)*f(b);
}
}

class Filter {
private: Left is non-inlined class Filter {
private:
Function fn;
public:
version, right is the Function fn;
public:
inline double evaluate( double t ) { same with inline inline double evaluate( double t ) {
return integrate( fn, t, t+1e-5 );
} keywords added return integrate( fn, t, t+1e-5 );
}
};
};
main() {
main() {
Filter f;
Filter f;
double total = 0.;
double total = 0.;
for( int i = 0; i < 100000; i++ ) {
for( int i = 0; i < 100000; i++ ) {
total += [Link]( double(i/1e6) );
total += [Link]( double(i/1e6) );
}
}
cout << total;
cout << total;
}
[Link] } Inlining_inline.cpp
INSTANCE VS POINTER
• A related example is passing an object, a pointer
or a reference to a function

Program compiled, then


disassembled and decompiled
using Ghidra – this generates
equivalent source code for the
instructions the processor
actually generates
FUNCTION DECOMPILATION Filter::evaluate()

integrate()

main()

operator() ()

Function::evaluate() Compiled code has exactly the same structure as that I wrote
Lots of function calls
I didn’t try to make this happen – just used default options on g++ and didn’t use inline hints
WHAT DOES A FUNCTION LOOK LIKE TO A
PROCESSOR?
• Previous slide shows that without inlining we get a lot of function call, but what exactly is the
overhead associated with each?

• In Ghidra, I can see the assembly code alongside the decompiled code.
• Assembly is the instructions that the processor will actually execute
• Decompiled code is the equivalent human readable source code

• If we look at this for one function, we can see:


• What the processor has to do
• How much of this actually relates to useful calculations
• How much is “overhead” – setting the function up
Function setup

Integral calculation

Function return
INSTANCE VS POINTER
• A related example is passing an object, a pointer
or a reference to a function

This is the program


disassembled and decompiled
when inlining is requested.

Use of inline keyword and –O1

Note that there is no reference


to classes in the decompiled
code, it has been reduced to a
loop in main()
THE REAL IMPACT?
main() {
Filter f;
double total = 0.;
time_t c;
double t;
• I did some timing on this:
c = clock();
• I added basic timing code for( int i = 0; i < 10000000; i++ ) {
total += [Link]( double(i/1e6) );
• I increased the number of loops performed (to }
c = clock()-c;
increase accuracy of timing) t = (double) c / CLOCKS_PER_SEC;

cout << total << " time: " << t << endl;
}
• Non-inlined program: 123ms
• Inlined program 22ms

• >5.5X slower!
AN ADMISSION (AND A WARNING!)
• I said that I was showing you effect of inlining – this was true. I also talked about use of the
inline keyword but the truth here is that it didn’t actually do anything!
• I did two things:
• Used inline / didn’t use inline
• I passed an option to the compiler on the comment line (-O1) – this means use optimisation
level 1
• The compiler by default uses optimisation level 0 (i.e. no optimisation). For this program, that
appears that it will not inline at this level, even if you use the inline keyword.
• When you enable optimisation level 1, it chooses to inline. Whether you use the inline keyword
or not!
• Important point 1: I did mention this earlier – but inline is just a hint, the compiler will still do [1]
what it thinks is appropriate. [Link]
• Important point 2: If speed really is an issue for some of your code:
n-
• Profile the code, this can be as simple as putting timing calculation code in (temporarily). IDEs
us/visualstudio/profiling/profi
like Visual Studio have code profiling features [1]. ling-feature-tour?view=vs-
• Use a decompiler like Ghidra to see what the compiler actually generated – it might be quite 2022&pivots=programming-
different to the way you structured your original source code language-dotnet
OTHER TEMPLATE TYPES
Additional types of template
ADDITIONAL / ALTERNATIVE PARAMETERS
• Possible to have multiple parameters in a template template <typename T, typename U>
• T and U are potentially different types here U evaluate( T x, U p1 ) {
• Return type is same as that of second parameter (x > 0 ) ? p1 : -p1;
}
• Template parameter can be numeric
• It says that function/class will contain a number, but not what
that number is
• Why do this? These parameters are inserted by the compiler, template <typename T, int n>
they are not variables.
• Affects how code is compiled
class Function {
• In this case, it means the array is statically allocated, not
private:
dynamically allocated at runtime T workingArray[n];
• A bit like a fancy #define public:
T evaluate( T x );
};
• Parameter can also be a bool
TEMPLATE INSTANTIATION - NUMERIC
• The compiler must be able to determine all template<int order>
template parameters at compile time, they float power( float x ){ // Remember for different orders
cannot be variables whose value will be we have a different
// function
determined when the program runs return( pow(x,order) );
}

main(){
• You cannot do this -> int n;

• In this case, n is not known until the cout<<"\nPlease enter the order";
program runs.
cin>>n;
• But, the template parameter must be
cout<<"\n"<<power<n>(7); // Here n is a variable !!!
resolved by the compiler Must be a compile time constant!
}
TEMPLATE INSTANTIATION - NUMERIC
• The compiler must be able to determine all
template<int order>
template parameters at compile time, they float power( float x ){ // Remember for different orders
cannot be variables whose value will be we have a different
determined when the program runs // function
return( pow(x,order) );
}

main(){
• This is possible, as all instances of the int n;
templated function can be generated at cout<<"\nPlease enter the order";
compile time
cin>>n;
• But both versions of the function are
included in the compiled program and the switch(n) {

decision about which version of the function case 0:


is made a run-time cout<<"\n"<<power<0>(7);break;
case 1:
cout<<"\n"<<power<1>(7);break;
etc
}
}
TEMPLATE PARAMETERS ARE NOT MEMBER
VARIABLES template<typename T, int order>
class Power {
• You cannot access the template parameter as
if it was a variable. public:
T operator()(T x) {
return( pow( x,order ) );
}
• If you want to know what the “power” };
parameter was in this case, you’d need to add
an accessor function which referred to the
main(){
parameter Power <double,3> pf;

cout<<"\n"<<[Link]<<"" "<<pf.T;
}
TEMPLATE PARAMETERS ARE NOT MEMBER
VARIABLES template<typename T, int order>
• You cannot access the template parameter as if class Power {
it was a variable.
public:
T operator()(T x) {
return( pow( x,order ) );
• If you want to know what the “power” parameter }
was in this case, you’d need to add an accessor
function which referred to the parameter int getOrder() {
return order;
}
};
• This works because the compiler will insert the
templated value of order when the code for main(){
getOrder is compiled Power <double,3> pf;

cout<<"\n"<<[Link]()<<""
"<<pf.T;
}
OTHER POINTS template <typename T = double> class Function {
private:
T value1;
• Default template types T value2;

• Force the template to a specific type by // Constructor


Function(T val1, T val2) : value1(val1), value2(val2) {}
default };

Function f; // this cant work, as there isnt enough


// information to determine the type T
Function<int> fi; // T will be an int
Function<float> ff; // T will be a float this time
Function fd; // T will default to a double

• Template variables
template <typename T> constexpr T pi = T(3.141592);
• E.g. define a number which can
become a float/double as required cout << pi<double> << " " << pi<float> << "\n";

• Flexible way of defining constants


RECURSIVE TEMPLATES
RECURSIVE TEMPLATES Vout ( z ) =
a0 + a1 z + a2 z 2 .... + aM z M
Vin ( z )
1 + b1 z + b2 z 2 .... + bN z N
• Take a IIR style digital filter as an example
General IIR transfer function, z is the
sample number
• Related to previous examples in that we are still talking about a
filter, but:
• Digital filter deals with signals in terms of sample number (integer)
rather than continuous time (floating point) M N
V i
out = a V i −m
m in −  bnVout
i −n

m =0 n =1
• The current output is related to previous inputs and outputs as a
sum. As an “algorithm” or “difference
function”, the output at the current
time (sample i) is related to the N
• The number of previous inputs and outputs is variable (represented previous outputs and M previous
by M and N in this case) inputs

• M+N coefficients are used to represent the contribution of the


previous inputs and outputs.
A BASIC IIR CLASS // Operator () definition
double IIR::operator()( double ip ){
// Loops to evaluate output given current input ...
}

class IIR {
public:
IIR( int _M,int _N, double* _a, double* _b );
double operator()( double ip ); // Constructor definition
IIR::IIR( int _M,int _N, double* _a, double* _b ) : M( _M), N (_N) {
private: a = new float[M];
// Arrays to store previous input/output b = new float[N];
samples
int M, N; vin = new float[M];
double *vin; vout = new float[N];
double *vout;
for( int m = 0; m < M; m++ ){
// Arrays to store filter coefficients a[m] = _a[m];
double *a; vin[m] = 0;
double *b; }
};
for( int n = 0; n < N; n++ ){
b[n] = _b[n];
vout[n] = 0;
}
}
ISSUES WITH THE BASIC IMPLEMENTATION
• M and N can be anything – up to the user to decide

• Higher order IIR filters (large M,N) can be numerically unstable:


• This means that even if the filter is theoretically stable, the computer implementation becomes unstable
• Occurs due to rounding errors in long calculations – rounding errors accumulate in high order terms and result in accumulating errors

• A solution is to split the high-order approach into bi-quadratic filters (𝑀, 𝑁 = 2)

• The product of these filters is equivalent to higher order filters

• The reason for mentioning this is not filter design theory, its just a hypothetical example to illustrate recursive templates

a0 + a1 z + a2 z 2 .... + aM z M  u0 + u1 z + u2 z 2   p0 + p1 z + p2 z 2 
Vout ( z ) = Vin ( z ) =  2 
....  V ( z)
2  in
1 + b1 z + b2 z .... + bN z
2 N
 1 + v1 z + v2 z   1 + q1 z + q2 z 
IIR FILTERS USING RECURSIVE BIQUADS

 u0a + u1a + u2a z 2   u0b + u1b + u2b z 2   u0c + u1c + u2c z 2 


Vout ( z ) =  a 2 
 b 2 
  V ( z)
c 2  in
 1 + v1 z + v2 z   1 + v1 z + v2 z   1 + v1 z + v2 z 
a b c

input to
BQc
The individual filter input to
actions can be BQb BQc
represented by input to
something similar to BQa BQb
the Filter class from BQa
earlier

Algorithmically: vout = biquada( biquadb( biquadc(vin) ) )

How do you create a class that chains or cascades with instances of itself?
How do you code this so that it is easy to initialise and use these chains?
IIR FILTERS USING RECURSIVE BIQUADS

 u0a + u1a + u2a z 2   u0b + u1b + u2b z 2   u0c + u1c + u2c z 2 


Vout ( z ) =  a 2 
 b 2 
  V ( z)
c 2  in
 1 + v1 z + v2 z   1 + v1 z + v2 z   1 + v1 z + v2 z 
a b c

input to
BQc
input to
BQb 𝑁=1 BQc
input to
BQa 𝑁=2 BQb
𝑁=3 BQa
template<int N, typename Filter, typename T, typename>
• Each level represented by a cascade structure, struct Cascade {
each containing a digital filter // Digital filter, this can be a variant of the earlier filter class
Filter filter;
• Each cascade also contains the cascade // "The object whose output is my input"
immediately after it – it is able to call the next filter Cascade<N-1,operation,T> next;
in the chain
T operator()( T t ){ // "My output" - my operation on my input
return( filter( next( t ) );
}
};
BIQUAD FILTER CLASS
// create a class that acts as the digital filter for each level
• Need a filter class, call it template<typename T>
class BiQuad {
BiQuad private:
T u2, u1, u0; // three coefficients for numerator
T v2, v1; // two coefficients for numerator
T vin1, vin2; // past values for vin
T vout1, vout2; // past values for vout
• Implements filter function
public:
// constructor that sets up coefficient values
𝑢0 + 𝑢1 𝑧 + 𝑢2 𝑧 2 BiQuad( T _u0, T _u1, T _u2, T _v1, T _v2 )
𝑉𝑜𝑢𝑡 = V : u0(_u0), u1(_u1), u2(_u2), v1(_v1), v2(_v2), vin1(0), vin2(0), vout1(0), vout2(0) {
1 + 𝑣1 𝑧 + 𝑣2 𝑧 2 in }

𝑉𝑜𝑢𝑡 = 𝑢0 𝑉𝑖𝑛 𝑖 + 𝑢1 𝑉𝑖𝑛 𝑖 − 1 + u2 Vin i − 2 T operator() (T vin) {


// Compute output
−v1 Vout i − 1 − v2 Vout [i − 2] T vout = u0*vin + u1*vin1 + u2*vin2 - v1*vout1 - v2*vout2;
// Save previous values
vin2 = vin1; vin1 = vin;
• Variables to store vout2 = vout1; vout1 = vout;
// return output
parameters and past return vout;
}
input/output values };
CASCADE CLASS
template<int N, typename Filter, typename T>
• Each Cascade class takes N as a template class Cascade {
argument – its position in the chain private:
// Instance of filter for this level
Filter f;
• It contains another Cascade class, with N-1 Cascade<N-1, Filter, T> next;
public:
in the chain T operator() ( T vin ) {
• Create a Cascade with N=3 return f( next(vin) );
• It contains one with N=2 }
};
• Which contains one with N=1
• Which contains one with N=0
• Which contains one with N=-1
• …. This is a problem!
• If we create Cascade with N=3, want the other two Cascades to be created
(N=2,N=1) to complete set of 3
• Code as it is will continue indefinitely
• Compiler will throw an error after a certain number of recursions (default in
900 in MinGW-64)
TEMPLATE SPECIALISATION
template<int N, typename Filter, typename T>
class Cascade {
• Template specialisation allows special private:
versions of a templated class to be used in // Instance of filter for this level
Filter f;
certain circumstances Cascade<N-1, Filter, T> next;
• E.g. when N has the value 1 public:
T operator() ( T vin ) {
return f( next(vin) );
• A special version of class where N is fixed to };
}

1 is defined
• This is used when N=1 template<typename Filter, typename T>// N is no longer a template parameter
• This instance stops the cascade, there class Cascade<1,Filter,T> { // N is specified for this version of class
private:
is no “next” instance // Instance of filter for this level
Filter f;
// no next class
public:
T operator() ( T vin ) {
return f( vin );
}
};
[Link]

CONSTRUCTOR FOR RECURSIVE CLASSES


main() {
• Initialise the cascade with an array of // Create an array of Biquad filters, each is initiaised with a set of
// value for the coefficients
Filters BiQuad<double> cascadeElements[3]
= { BiQuad<double>(1., 1., 1., 1., 1.),
BiQuad<double>(1., 1., 1., 1., 1.),
• Each cascade copies the relevant Filter, BiQuad<double>(1., 1., 1., 1., 1.)
};
based on its value for N
// Create a cascade to house these filters
Cascade <3, BiQuad<double>, double> filterCascade( cascadeElements );
• Again, special case for N=1
cout << filterCascade( 3. );
}

template<int N, typename Filter, typename T> template<typename Filter, typename T>


class Cascade { class Cascade<1,Filter,T> {
private: private:
Filter f; Filter f;
Cascade<N-1, Filter, T> next; public:
public: Cascade( Filter* filters ) : f( filters[0] ) {
Cascade( Filter* filters ) : f( filters[N-1] ), next( filters ) { }
}
T operator() ( T vin ) {
// Last cascade doesnt need to pass vin down to anything
T operator() ( T vin ) {
return f( vin );
return f( next(vin) );
}
}
};
};
WHY? AND SUMMARY
• Why are we looking at this?
• Its an example of recursive templates – these are used, so its useful to
know about them
• Its an example of template specialisation (partial specialisation in this case)
• We’ll come back to this later
• This type of approach can allow you to make complex algorithms/structures,
using small and modular classes
• This is an example of scalable software (complexity)
• The classes actually concatenate lots of simple functions like the previous
inlining example
• So there is a good chance, with a appropriate compiler options, that the code
compiles to efficient code – another example of scalable software (efficiency)
• OOP can give you efficient code, with nice object structure that allow easily
reusable code
• It won’t always, so you need to think about what you are doing

You might also like