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,...).
4 Answers 4
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.
-
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 whatstd::min()
does. \$\endgroup\$G. Sliepen– G. Sliepen2021年09月02日 12:48:54 +00:00Commented Sep 2, 2021 at 12:48 -
1\$\begingroup\$ so using plain value /
double
would be the more "correct" / better version in this case? \$\endgroup\$Matthias Burger– Matthias Burger2021年09月02日 12:55:38 +00:00Commented 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\$Ralf Kleberhoff– Ralf Kleberhoff2021年09月02日 12:58:37 +00:00Commented 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– Ruslan2021年09月03日 12:02:04 +00:00Commented 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 literalsmin_value(&(double){x + 13}, &(double){2 * y})
, not that I reccomend it. \$\endgroup\$chux– chux2021年09月07日 19:14:52 +00:00Commented Sep 7, 2021 at 19:14
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.
-
2\$\begingroup\$ Oh the chief difference is nice.. I like! \$\endgroup\$Matthias Burger– Matthias Burger2021年09月02日 13:14:58 +00:00Commented Sep 2, 2021 at 13:14
-
3\$\begingroup\$ Modern calling conventions pass
double
s in registers (if there are few enough of floating-point parameters), which makes the question of their size not too relevant. \$\endgroup\$Ruslan– Ruslan2021年09月03日 12:05:45 +00:00Commented Sep 3, 2021 at 12:05
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.
Both compare functions lack good handling of all double
values.
Troublesome cases involve -0.0 and not a number:
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?calc_min_value(NotANumber, 42.0)
returns 42.0 - seems reasonable.calc_min_value(42.0, NotANumber)
returns NotANumber - inconsistent with #2. I'd expect #2 & #3 to return the same.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 ofisless(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.
static
modifier, thus with optimization, I suspect the assembler would probably be the same. \$\endgroup\$