I want to call a function more than once, but it uses static variables.

I am trying to simplify some code for an embedded processor.
Currently it has several similar low pass filter functions which need to remember the last returned value each time they are called to calculate the new value. Some also need to remember multiple values, but I'm trying to strip my question down to the basics rather than open a can of worms.

Trying to avoid repeating myself and global variables. How would I write something that can be called to filter multiple inputs?
The function uses a static variable so it would mix all the signal values together. Writing several similar functions is repetition and in a larger program it gets confusing.

I've avoided structs and classes because I don't really understand them. I don't know how the variable's (object?) lifetime work and the vocabulary is confusing, like it seems different words are used to describe the same things. Methods and member functions are the same thing, right? They seem to be used interchangeably. I have several beginner C++ books which all basically end by touching on classes without really explaining the practical uses (to me anyway)
I have some more advanced books, but I feel I'm missing something more intermediate.
Are classes even a solution I should be looking at? Am I even asking the right question?

This is stripped down sample code. The analog_input_value() is there as an example, but doesn't actually work. That would be more complicated, but it would just add a distraction here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
float LPFfilter (float input, float coef)
{
  static float lpfBuffer;
  lpfBuffer = lpfBuffer + ((input - lpfBuffer) * .coef);
  return lpfBuffer;
}

int main ()
{
  while (1)
    {
      float a = analog_input_value();	// example as a signal read from ADC
      a = LPFfilter(a, .045f);          // new analog value and filter coefficient
      cout << a << endl;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LowPassFilter{
    float coeff;
    float state = 0;
public:
    LowPassFilter(float coeff): coeff(coeff){}
    LowPassFilter(const LowPassFilter &) = default;
    LowPassFilter &operator=(const LowPassFilter &) = default;
    float operator()(float input){
        return this->state = this->state + ((input - this->state) * this->coeff);
    }
};

int main (){
    LowPassFilter f(.045f);
    while (1){
        float a = analog_input_value();	// example as a signal read from ADC
        a = f(a);                       // new analog value
        cout << a << endl;
    }
}
Last edited on
Trying to avoid repeating myself and global variables. How would I write something that can be called to filter multiple inputs?

Replace the static variable with a parameter managed by the caller.
1
2
3
4
5
6
float make_filter_context() { return 0.f; }

float lpfilter(float* ctx, float input, float coef)
{
  return *ctx += (input - *ctx) * coef; 
}


To be used as
1
2
float context = make_filter_context();
std::cout << lpfilter(&context, analog_input_value(), .045f);

The variable context represents the state of the filter. You can use this method to filter multiple sequences, as long as you use a separate context variables for each sequence -- and thereby keep their states distinct.

If you're implementing some filter which needs to recall the last ten values and a coefficient, you would generally bundle this data up, define a function to set its initial values, and apply the same idiom:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct filter_context 
{
  float history[10];
  float coef; 
};

// create a filter_context and set its initial state
filter_context make_filter_context() 
{
  filter_context x; 
  for (float& elt: x.history) elt = 0.f;
  x.coef = 0;
  return x;
}

// apply the filter whose state is defined by *ctx to the input
float magic_filter(filter_context* ctx, float input);

int main()
{
  filter_context context = make_filter_context();
  std::cout << magic_filter(&context, analog_input_value()) << '\n';
}


In this example, filter_context exists only to bundle related data together. It simply holds all the filter's state, and gives the filter function a place to store it for next time. This is a common idiom in both C and C++: the class exists to bundle data together so it can be handled more readily as a unit.

The prior example is both a C program and a C++ program. The C++ solution is a bit better, but the essence of the solution is already here:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct filter_context
{ 
  std::array<float, 10> history{};
  float coefficient = 0.f;
};

float magic_filter(filter_context& ctx, float input); 

int main()
{
  filter_context context;
  std::cout << magic_filter(&context, analog_input_value()) << '\n';
}
Here in-class initializers are used to eliminate the handwritten make_filter_context function, and a reference parameter is used to indicate that a null pointer is not an acceptable argument to magic_filter.

The vocabulary is confusing
C++'s terminology is somewhat different from that of other communities. There is a precise C++-specific terminology, which I use in all my answers here, and a generic terminology for object-oriented programming, which I try to avoid.

I've avoided structs and classes because I don't really understand them. I don't know how the variable's (object?) lifetime work

The lifetime of an object is unrelated to its type. For example, whether an object has fundamental type (e.g., float) or class type (e.g., filter_context) doesn't make any difference. You may treat filter_context like a float -- at least as far as its lifetime is concerned.

Methods and member functions are the same thing, right? They seem to be used interchangeably
Method is a generic term for member functions.

Last edited on
Replace the static variable with a parameter managed by the caller.

To be used as

The variable context represents the state of the filter. You can use this method to filter multiple sequences, as long as you use a separate context variables for each sequence -- and thereby keep their states distinct.


On the first example I don't understand this function.
float make_filter_context() { return 0.f; }

When called with
float context = make_filter_context();
this would simply return a float value of zero, right? I'm sure I'm missing something.

On the last example
Here in-class initializers are used to eliminate the handwritten make_filter_context function, and a reference parameter is used to indicate that a null pointer is not an acceptable argument to magic_filter.

You mention class, but the code shows a struct. I'm under the impression that they can often be used similarly, but are each their own distinct things, or am I mistaken?
I feel like I'm almost following the last example, but I don't understand how the lifetimes work.

If I declare a variable in a function its lifetime ends with the function. Globals would last as long as the program is running. Static will last as long as the program is running, but they can be used locally. Do struct variables (I'm probably butchering the proper jargon) have a finite lifetime? I notice the passing by reference. My only practice with this is when I need more than one return from a function. Am I asking the right question or just confusing myself?

On the first example I don't understand this function.

It's presence informs programmers that they must use the results of that function as lpfilter's first argument. It is not necessary, but I included it
a.) for similarity with the following examples; and
b.) because it's part of a common pattern you'll see again.

Much of this isn't explained -- it's just a common convention.

You mention class, but the code shows a struct. I'm under the impression that they can often be used similarly, but are each their own distinct things, or am I mistaken?
They're identical, except that by default, class has a different default access specifier.

Any other differences are merely conventions. IMO these conventions are not useful for improving software, and so they're very unimportant. To summarize, class & struct are effectively the same.

I feel like I'm almost following the last example, but I don't understand how the lifetimes work. [...] Do struct variables (I'm probably butchering the proper jargon) have a finite lifetime?

If we have a class (or struct - they're the same):
struct A {};
Then the lifetime of (the object referred to by) a variable with type A depends on how it was created:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// variable at namespace scope is implicitly static;
// its lifetime is (basically) the same as the program
A a;

void f()
{
  // variable with automatic storage duration at block 
  // scope -- lifetime is the same as the block
  A b; 
  { 
    A c; // block scope
  } // c's lifetime ends here
} // b's lifetime ends here

void g()
{
  // variable with static storage duration at block scope
  // lifetime begins when control first reaches the definition
  // (line 20) and ends when the program ends
  static A d;
}

A* h()
{
  // new A creates an object with dynamic storage duration
  // lifetime begins at the new-expression (line 28); 
  // ends at the delete-expression in the main function
  return new A; 
}

int main()
{
  f();
  g();
  A* pa = h();
  delete pa; // lifetime of the object pointed-to by pa ends here
}


I notice the passing by reference. My only practice with this is when I need more than one return from a function.
This is exactly what's happening. The argument is read, modified, and "returned" to the caller.
Last edited on
If I declare a variable in a function its lifetime ends with the function. Globals would last as long as the program is running. Static will last as long as the program is running, but they can be used locally. Do struct variables (I'm probably butchering the proper jargon) have a finite lifetime?

To be clear: the rules for scope and lifetimes of structs/classes are exactly the same as for primitive variables (e.g. int).

If you declare a struct object as a local variable, it will be destroyed when it goes out of scope - just like a local int variable would.

If you declare a struct object as a global variable, it will persist for the lifetime of the program - just like a global int variable would.

If you declare a struct object as a static variable, it will persist for the lifetime of the program, but only be accessible from within the local scope - just like a static int variable would.

etc, etc...
Last edited on
Topic archived. No new replies allowed.