6
\$\begingroup\$

I'm learning C and I just had the exercise to write a function that checks for the lower one of two floating-points. So I thought, I either could use pointer and also return the pointer of the lower value - otherwise, I simply could use the values and return the lower value.

double* pointer_min_value(double* first, double* second){
    return *first > *second ? second : first;
}

double calc_min_value(double first, double second){
    return first > second ? second : first;
}

int main() {
    printf("Enter two numbers to define the minimum!\n");

    double first, second;
    scanf("%lf%lf", &first, &second);

    printf("The lower value is %lf", *pointer_min_value(&first, &second));
    printf("The lower value is %lf", calc_min_value(first, second));

    return 0;
}

they both work, but what would make more sense? should I use pointer only if I want to change the value (I mean, I should not change any of the parameters in a "get me the lowest number"-funtion), or would it be better in any other view (resources, performance,...).

\$\endgroup\$
1
  • 1
    \$\begingroup\$ This will probably be a candidate for a static modifier, thus with optimization, I suspect the assembler would probably be the same. \$\endgroup\$
    – Neil
    Commented Sep 2, 2021 at 14:57

4 Answers 4

16
\$\begingroup\$

Versatility

The pointer version is less versatile.

If the two values come from expressions like in min_value(x + 13, 2 * y), you can't use the pointer version, as neither x + 13 nor 2 * y exist as pointable memory locations, only as temporary values.

Surprises

The pointer version might give surprising results in the long run.

If now first is lower than second, the function will return a pointer to first. If you later change the content of the first variable, the value of the result changes as well.

If first and second are local variables declared inside a function foo(), and you happen to return the pointer_min_value() result (the pointer), then, after leaving foo(), the variables no longer exist, and your result points to garbage memory.

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Note that using const references avoids those surprises, and gives you value semantics, while still technically using pointers under the hood. This is for example what std::min() does. \$\endgroup\$
    – G. Sliepen
    Commented Sep 2, 2021 at 12:48
  • 1
    \$\begingroup\$ so using plain value / double would be the more "correct" / better version in this case? \$\endgroup\$ Commented Sep 2, 2021 at 12:55
  • 4
    \$\begingroup\$ @MatthiasBurger Yes. Generally, that depends on the intended usages of the function, but I can't imagine a convincing one in favor of the pointers version. \$\endgroup\$ Commented Sep 2, 2021 at 12:58
  • 3
    \$\begingroup\$ @G.Sliepen this is C, not C++. Though, const pointers would still be relevant indeed. \$\endgroup\$
    – Ruslan
    Commented Sep 3, 2021 at 12:02
  • \$\begingroup\$ " you can't use the pointer version" is amiss. To use x + 13, 2 * y with the pointer version, code can use compound literals min_value(&(double){x + 13}, &(double){2 * y}), not that I reccomend it. \$\endgroup\$
    – chux
    Commented Sep 7, 2021 at 19:14
10
\$\begingroup\$

Some beautifying

I think first < second ? first : second is a tiny bit more readable. First comes first.

Double problems

Floating points are tricky. What will happen if one of doubles will be NaN? Of course, you can always say that the function assumes both values are valid, but you should consider this too.

Pointer problems

Pointers are also tricky. pointer_min_value(NULL, NULL) will cause UB. Once again, adding a comment "no NULLs allowed" is ok - but neglecting this possibility isn't.

Making sure the values, passed by pointers, won't change

You can add const modifier to arguments and returning value to do this, if you mean this.

Performance considerations

Passing a huge argument by value can be a bad idea; but doubles are only 8 bytes - like pointers on 86x64 architecture; pointers on 86x32 are 4 bytes, so you can save 8 bytes of stack space with pointer_min_value. But who cares with modern RAM sizes?

On the other hand, dereferencing pointers isn't free, so calc_min_value will be a bit faster, but who cares with modern CPU/RAM speeds and cache sizes?

Chief difference

The only thing pointer_min_value does that calc_min_value doesn't - it returns a pointer. Why is this important? Because you can change the returned value!

*pointer_min_value(&first, &second) = 0.0;

changes lesser one to 0.0. That's the greatest difference.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Oh the chief difference is nice.. I like! \$\endgroup\$ Commented Sep 2, 2021 at 13:14
  • 3
    \$\begingroup\$ Modern calling conventions pass doubles in registers (if there are few enough of floating-point parameters), which makes the question of their size not too relevant. \$\endgroup\$
    – Ruslan
    Commented Sep 3, 2021 at 12:05
5
\$\begingroup\$

It's a good practice to always pass primitive variables by values. Passing pointers should be reserved for objects that take more space such as arrays or structs. It also makes functions and their calls more readable. If you nitpick, you could also make an argument that something like an int usually uses 4 bytes of memory while a pointer usually uses 8, so the function is slightly more memory hungry.

\$\endgroup\$
0
1
\$\begingroup\$

Both compare functions lack good handling of all double values.

Troublesome cases involve -0.0 and not a number:

  1. calc_min_value(-0.0, +0.0) returns 0.0. OK, but not clear from the function specification. -0.0 is reasonable - but worth the effort?

  2. calc_min_value(NotANumber, 42.0) returns 42.0 - seems reasonable.

  3. calc_min_value(42.0, NotANumber) returns NotANumber - inconsistent with #2. I'd expect #2 & #3 to return the same.

  4. calc_min_value(NotANumberA, NotANumberB) returns NotANumberB. Rare case when 2 NANs are present. Slightly more common to return the first when both quiet or both signaling NANs, else the signaling one.

Using a return of a pointer does allow returning of the first address, second address, or NULL - perhaps for NAN cases. Yet a three way return may be clumsy for the user.


FP functions are most commonly handled with numeric parameters and return - not an address.

Note: Compare of numeric values often involves a constant. Forming the address is more work.


Consider returning the lesser of the two when they have different values, -0.0 when tied with 0.0, else the first when equal and the non-NAN when one of them is NAN.

Possibly may want to return -NAN over a NAN when both NAN (not shown).

Pedantic code checks for NAN-ness first as first < second is not specified by C concerning NANs, yet that compare is very commonly false.

// More robust
double calc_min_value(double first, double second) {
  if (isnan(first)) {
    if (isnan(second)) return first;
    return second;
  }
  if (isnan(second)) {
    return first;
  }
  if (first < second) return first;
  if (second < first) return second;

  // If both the same sign, return the first.
  if (!signbit(first) == !signbit(second)) return first;
  return signbit(first) ? first : second;
}

Research also isless()

The isless macro determines whether its first argument is less than its second argument. The value of isless(x,y) is always equal to (x)< (y); however, unlike (x)< (y), isless(x,y) does not raise the "invalid" floating-point exception when x and y are unordered and neither is a signaling NaN.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.