Ellie Cronk
Ellie Cronk

Reputation: 11

Saving a value between function calls in C

Is there any difference in functionality between these two sections of code? The main difference is the use of 'static' in the first example to save the value of x each time function1 is called. But the second example removes the need to use 'static' altogether by passing the value of i from main to function1 on each iteration of the for loop. The both have exactly the same output. Is there any hidden advantage to using one way over the other?

Note: the first example is a simplified version of a piece of code I've seen. Just wondering why this was used rather than the alternative.

First example:

void function1()
{
    static int x = 0;
    printf("function1 has now been called %d times\n", ++x);
}

int main(void)
{
    for (int i = 0; i < 5; i++)
        function1();
    return 0;
}

Second example:

void function1(int i)
{
    printf("function1 has now been called %d times\n", ++i);
}

int main(void)
{
    int i;
    for (i = 0; i < 5; i++)
        function1(i);
    return 0;
}

I'd appreciate any shared knowledge.

Upvotes: 1

Views: 6316

Answers (1)

user1902824
user1902824

Reputation:

As people stated in the comments, there are pros/cons to each approach. Which one you choose depends on the situation you are in, and what tradeoffs you are willing to make. Below are a few ideas to get you rolling.

Static variable approach

void function1(void)
{
    static int x = 0;
    printf("function1 has now been called %d times\n", ++x);
}

Pros:

  • Lower resource usage: You aren't passing x on the stack, so you use less memory, if memory is a premium. Additionally, you save a few instructions moving the argument onto the stack. The address is also fixed, so you don't need to store it or manipulate it in code.

  • Good locality: As a static, x remains minimally scoped to it's purpose, meaning the code is easier to understand and debug. If the scope of x was increased to the entire file, it would be a lot harder to understand (who is modifying x, how can it change, etc).

  • More flexible architecturally (simple interface): There really isn't a wrong way to use this function — just call it. Don't need to worry about validating inputs. If you need to move it around, just drop in the header and you are good to go.

Cons:

  • Less flexible functionally: If you need to change the value of x, for example, needing to reset it, you don't have a way to do that. You would need to alter your design somehow (make x a global, add some reset parameter to the function, etc) to make it work. Requirements change all the time, and the one that can be minimally changed to do what you want is a hallmark of good design.

  • Harder to test: Tying into the point above, how would you unit test this function? If you wanted to test some low numbers and some high numbers (typical boundary tests), you would need to iterate through the entire space, which may not be feasible if the function takes a long time to run.

Argument approach

void function1(int x)
{
    printf("function1 has now been called %d times\n", x);
}

void caller(int *x) /* could get x from anywhere */
{                   /* showing it as a pointer from outside here */
    *x = *x + 1;
    function1(*x);
}

Pros:

  • More flexible functionally: If your design requirements change, it's relatively simple to change. If you need to change the sequence or have special conditions, like repeat the first value or skip a particular value, that's really easy to do from the calling code. You've separated out things so you can bolt on a new driver and don't need to touch this function anymore. Things are more modular.

  • More testable: The function can be tested much easier, granting more confidence that it actually works. You can do boundary testing, test inputs you are worried about, or recreate a failure scenario with ease.

  • Easier to understand: Functions in this format have the ability to be easier to understand. If a function produces the same output for a given set of inputs, it is said to be pure. The example given is not pure, because it is doing IO (printing to the screen). However, generally speaking, a pure function is easier to reason about because it doesn't hold any internal state, so the only things you really care about are the inputs. Just like 1 + 1 = 2, a pure function has this same simplifying property.

  • (Potentially) more performant: In the case of pure functions, compilers can take advantage of the function's referential transparency (a fancy word meaning you can replace add(1,1) with 3) and cache results from previous calls safely. Why do the same work again if you've already done it? If calling the function was particularly expensive and sat in a tight loop that called it with similar arguments, you've just saved tons of cycles. Again, this function is not pure, but even if it was, you wouldn't get any performance boosts since it is a sequential counter. Any benefits would be seen when the counter wraps, or the function is called with the same arguments again.

Cons:

  • More resource usage: If you are squeezed for memory, you utilize a little more stack space as you pass the variable over. You also use more instructions keeping track of it's address and moving it over.

  • Easier to screw up: If you only support numbers 1-10, someone is going to pass it 0 or -1. Now you need to decide how to handle that.

  • (Potentially) less performant: You can also bog down your code handling cases that should never happen in the name of defensive programming (which is a good thing!). But generally speaking, defensive programming programming is not built for speed. Speed comes from carefully thought out assumptions. If you are guaranteed that your input falls in a certain range, you can keep things moving as fast as possible without pesky sanity checks peppering your pipeline. The flexibility you gain from exposing this interface comes at a performance cost.

  • Less flexible architecturally (more cruft): If you call this function from a lot of places, now you need to string along this parameter to feed to it. If you are in a particularly deep call stack, there can be 20 or more functions which pass this argument along. And if the design changes and you need to move this function call from one place to another, you have the pleasure of ripping out the argument from the existing call stack, changing all the callers to conform to the new signature, inserting the argument into the new call stack, and changing all it's callers to conform to the new signature! Your other option is to leave the old call stack alone, which leads to harder maintainability and a higher "huh" factor when someone peruses the extra baggage from the days of yore.

Upvotes: 1

Related Questions