I am trying to convert a floating point number to an integer, but it seems like the conversion is not working properly.
I am using the math library to calculate the power of 10 based on the number of varargs, where the first argument is the number of arguments followed by the varargs.
int charsToIntValue(int argc, ...) {
int multiplier = (int) pow(10, argc - 1);
va_list argp;
va_start(argp, argc);
Serial.println(multiplier);
...
va_end(argp);
}
For some reason, multiplier
is always stored as 1 less than the actual result.
e.g. If charsToIntValue(4, 1, 2, 3, 4)
is executed, the multiplier is stored as 999 rather than 1000. However, when it is printed as a float, it does show 1000.00. And whenever I change it to a constant ((int) pow(10, 3)
) it works fine, returning 1000.
I've tired storing the value of argc into a local variable but that didn't seem to change anything.
Full project is here for context: https://github.com/Pyrodron/charger-destination-board
File in question is charger-destination-board.ino; lines 32 to 51
2 Answers 2
With some inspiration from timemage's comment, I decided in the long run to implement my own power function that takes two int
arguments and returns an int
. Rather than use the pow
function in the <math.h>
library which returns a double
when I only need an int
.
int power(int x, int y) {
return y == 0 ? 1 : x * power(x, y - 1);
}
Now the program works as intended!
-
1I'd had some doubt about this actually solved the problem, because I'd tested the handful of exponent values you were using to see if the
pow()
happened to produce the values you wanted. Turns out avr-gcc will use higher precision if the optimizer can determine at compile time irrespective of whether it's doing this is aconstexper
context, meaning so much as having an argument something volatile qualified will causepow()
to evaluate at lower precision. Just more reason to not usefloat
on an AVR.timemage– timemage2021年02月02日 16:55:50 +00:00Commented Feb 2, 2021 at 16:55 -
1When the optimizer kicked in
pow(10, 4)
's result had significand field0b00111000011111111111110
and where the optimizer couldn't determine the exponent at compile time a significand field of0b00111000100000000000000
. The former converts toint
with the expected (for certain values of "expected") 10000 while the latter produces a value closer to 9984. Something also you should be aware of is that in avr-gcc, there really is nodouble
. It's a distinct type for overload purposes, but it's implemented as single precisionfloat
.timemage– timemage2021年02月02日 16:59:58 +00:00Commented Feb 2, 2021 at 16:59 -
@timemage all helpful information. Thank youDuluthIsSuperior– DuluthIsSuperior2021年02月04日 00:23:27 +00:00Commented Feb 4, 2021 at 0:23
As explained by @chrisl in a comment, the issue is that pow()
does not
return an exact result, only an approximation. It works fine with
compile-time constants because in this case it is evaluated at compile
time, by the compiler, on you PC.
Simple solution: replace the cast to int with round()
:
int multiplier = round(pow(10, argc - 1));
This works, but is awfully inefficient, as pow()
involves the
computation of both an exponential and a logarithm, both very expensive
on an AVR-based Arduino. The int-only function in your own answer is
certainly better. However, it is generally advised to avoid recursion on
low-memory Arduinos like the Uno. Prefer an iterative algorithm if you
can.
Also, the complexity of you int-only function is O(y). There is a well-known algorithm for integer powers that goes like O(log(y)):
int power(int x, int y)
{
int z = 1; // Invariant: result = x^y * z
while (y) {
if (y & 1) z *= x;
y >>= 1;
x *= x;
}
return z;
}
Maybe I am nitpicking, as your y
cannot go beyond 5 without
overflowing the result...
-
"works fine with compile-time constants because" as I said in the the comments on the O.P.'s own answer, it's actually more complicated than that. It has nothing to do with whether or not they're technically "constant". You can elicit the difference in this behaviour without crossing between constant expressions and non-constant expressions. The standard makes provisions for differences in precision between the two cases. So that is fin. But, the fact that it will give two completely different results outside the constexpr context is probably a bug.timemage– timemage2021年02月02日 17:43:52 +00:00Commented Feb 2, 2021 at 17:43
charsToIntValue
doesn't show me what you're talking about. I don't think this has anything strictly to do with variadic function support. Nor Arduino. I will just add thatpow(10, argc - 1);
could be better written as a small function that takes anint
, returns anint
, and consists of a switch statement with nofloat
at all.pow()
returns a float. If yes, that that behavior seems normal. Floats can only save an aproximation of the wanted number. The actual float value, that is calculated might be something like 999.99. When you them convert to an int, the digits behind the decimal point get thrown away, leaving 999. Thats something, that you would need to accept, when you want to work with floats. And thats one reason, why it is often better, to just use an integer representation.math.h
library. It works now!