Avoiding interpretation as octal in a C macro

215 views Asked by At

In an application I'm working on, we have a macro to convert clock times into an internal representation as a short integer:

#define CLOCK_TIME(hr, min) (s32)(((hr) * 60 + (min)) * 0x10000 / (24 * 60))

Naturally, we would write the hours and minutes exactly as they'd appear on a clock. So, for example, 6:30 would be CLOCK_TIME(6, 30). This all worked fine until someone tried to enter CLOCK_TIME(6, 08) and the compiler spat out an error saying 08 is not a valid octal constant. We tried a few workarounds, but they led to their own problems. For example, replacing min with 1##min - 100 fixed that specific issue, but then the macro would fail with any input that wasn't a 2-digit decimal literal. Is there a way we could allow 08 and 09 to work in the macro while also allowing arguments that may be single digits, expressions, or variables?

And yes, I realize just not using leading zeroes is a solution, but having the times appear the way they appear on a clock improves both readability and usability.

EDIT: One additional constraint is that if the time is a constant, it must be calculated at compile time and not runtime.

6

There are 6 answers

1
abelenky On

As @dimich suggested, you can append scientific notation e0.
This will first force the number to be a double, then you can type-cast it back to an int.

#define CLOCK_TIME(hr, min) (int32_t)(((hr) * 60 + (int32_t)(min##e0)) * 0x10000 / (24 * 60))

This works because 08e0 is valid as (double)8.000, even though 08 is invalid octal.

IDEOne Link

0
Eric Postpischil On

A solution for any number of digits (within implementation limits) (but not expressions or variables) is 1##min*2 - 2##min. This can be thought of as (10p+x)•2 − (2•10p+x), where p is the unknown number of digits in x, and it equals (2•10p+2x)−(2•10p+x) = 2xx = x. This avoids floating-point issues.

For example, 23 would be replaced by 123*2 - 223, which equals 23. 08 would be 108*2 - 208 = 8, and 8 would be 18*2 - 28 = 8.

A solution that handles numerals, expressions, and variables is to write your own script outside of C. Humans are not required to limit their programs to the C preprocessor. You can write your own script to remove leading zeros from numerals inside invocations of this macro and then use this script to process the source code before compiling it.

0
KamilCuk On

but then the macro would fail with any input that wasn't a 2-digit decimal literal

The following code:

#include <stdio.h>
#define POW10I(x) ( \
    x == 0 ? 1 : \
    x == 1 ? 1 : \
    x == 2 ? 10 : \
    x == 3 ? 100 : \
    x == 4 ? 1000 : \
    x == 5 ? 10000 : \
    x == 6 ? 100000 : \
    x == 7 ? 1000000 : \
    x == 8 ? 10000000 : \
    x == 9 ? 100000000 : \
    -1 )
#define TOBASE10IN(one_and_x, strx)   (one_and_x - POW10I(sizeof(strx)))
#define TOBASE10(x) TOBASE10IN(1##x, #x)

int main() {
    printf("%d\n", TOBASE10(0123456));
}

outputs 123456.

Is there a way we could allow 08 and 09 to work in the macro while also allowing arguments that may be single digits, expressions, or variables?

No.

If are willing to use runtime, you can then just do anything. Just parse the whole string representation of the expression and check if it is an octal number and take a different branch then.

#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
bool isdigits(const char str[]) {
    for (; *str; str++) {
        if (!isdigit(*str)) {
            return false;
        }
    }
    return true;
}
int tobase10(int x, const char xstr[]) {
    if (xstr[0] == '0' && isdigits(xstr)) {
        return strtol(xstr, NULL, 10);
    }
    return x;
}
#define TOBASE10(x) tobase10(x, #x)

int main() {
    printf("%d\n", TOBASE10(0123456));
    int a = 10;
    printf("%d\n", TOBASE10(a + a));
}

My take: I find it confusing that you want to change an internal part of C programming language. Octal numbers may be confusing for new users of the language, but I find that such macro usage will be confusing for the rest of the world. I would rather just leave a comment near the macro to watch out and learn about octal numbers.

0
n. m. could be an AI On

Is there a way we could allow 08 and 09 to work in the macro while also allowing arguments that may be single digits, expressions, or variables?

(Emphasis mine)

No, there is absolutely no way whatsoever to achieve that using C preprocessor.

0
Rachid K. On

Considering the values passed to the macro as digits, with gcc it is possible to make a conversion from string to integers suppressing the leading 0 thanks to a statement expression:

#define CLOCK_TIME(hr, min) ({ \
                   char __hr[3] = #hr, __min[3] = #min; \
                   int __hrv = 0, __minv = 0, __mul, __i; \
                   __mul = 1;__i = sizeof(#hr) - 2; \
                   while (__i >= 0) { __hrv += (__hr[__i --] - '0') * __mul; __mul *= 10; }  \
                   __mul = 1; __i = sizeof(#min) - 2;  \
                   while (__i >= 0) { __minv += (__min[__i --] - '0') * __mul; __mul *= 10; } \
                   (int)(((__hrv) * 60 + (__minv)) * 0x10000 / (24 * 60)); \
                            })

This solves the leading 0 problem but the calculation is done at runtime and this works only for digits passed to the macros (no variable or expression).

1
Lundin On

Defining this as macro rather than a function only makes sense if you intend for this number to be calculated at compile-time. So assuming that the macro will not get passed run-time variables but only digits at compile-time...

...then it is possible to conjure some evil magic macro language (not recommended) taking hh:mm as input but also allowing h:mm (but not hh:m). The caller side would look something like this:

#include <inttypes.h>
#include <stdio.h>

int main (void)
{
  printf("%" PRIi32 "\n", CLOCK_TIME(6:30));  // 17749
  printf("%" PRIi32 "\n", CLOCK_TIME(6:08));  // 16748
  printf("%" PRIi32 "\n", CLOCK_TIME(12:00)); // 32768
}

And the evil macro would look something along the lines of:

#define CALCULATE_STUFF(hr, min) (int32_t)(((hr) * 60 + (min)) * 0x10000 / (24 * 60))

#define STRLEN(str) (sizeof(str) - 1)
#define STR(arg) #arg

#define CLOCK_TIME(arg)                                              \
  STRLEN(STR(arg)) < 4 ? 0 :                                         \
  STRLEN(STR(arg)) > 5 ? 0 :                                         \
  STRLEN(STR(arg))==4 && STR(arg)[1]!=':' ? 0 :                      \
  STRLEN(STR(arg))==5 && STR(arg)[2]!=':' ? 0 :                      \
  CALCULATE_STUFF(                                                   \
    STRLEN(STR(arg))==4 ? (STR(arg)[0]-'0') :                        \
                          (STR(arg)[0]-'0')*10 + (STR(arg)[1]-'0')   \
  ,                                                                  \
    STRLEN(STR(arg))==4 ? (STR(arg)[2]-'0')*10 + (STR(arg)[3]-'0') : \
                          (STR(arg)[3]-'0')*10 + (STR(arg)[4]-'0')   \
  )

Explanation:

  • This macro returns 0 if incorrect, but could as well return some other integer to signal that something went wrong.
  • The error handling is based on "chaining" multiple ?: operators which has very low precedence, to the point where you can write pretty much anything as operands. It kind of works like a bunch of static assertions (and you could pass the result of the macro to _Static_assert if you fancy).
  • The STR helper macro converts the input to a string literal like "hh:mm" and the whole macro works on strings.
  • Therefore we can do a compile-time "strlen" call by taking the size of string literal, minus 1 for the null terminator.
  • First the macro checks that there's enough digits by calling a macro STRLEN and then that the : is in one of the allowed places.
  • Once the input format is considered ok, the actual calculation (which I didn't examine for correctness) is carried out in CALCULATE_STUFF.
  • CALCULATE_STUFF takes two arguments and depending on which input format that was used, it does a character to integer conversion, multiplying the first digit out of two by 10.

This macro comes with zero overhead and the CLOCK_TIME(6:30) etc calls are replaced with integers at compile-time.