2
\$\begingroup\$

For a program I'm writing, it requires a lot of printing and receiving user input. I've found it cumbersome to keep specifying the type strings in printf, and having to write another printf before using scanf to receive input. I've decided to write two macros, print and input, that achieve exactly what I want. A simplified function call that prints the types I need to print, and an input macro that allows me to pass a prompt and a variable to the same function.

Another concept I've included in the header file is pythons module-like structure. With specific defines, I can decide which functions I want to compile/use at a certain time. While there hasn't been a significant difference in compile speed when importing one function vs the entire file, my project will get exponentially large, so this might provide some time save. Is this something I should continue to implement? Or is the speed increase negligible to the point where it's a waste of time to write?

I would like to know if this is the best way to go about this. Having more experience in C++, I've learned to appreciate the nuances of C. Some of it looks convoluted, but I've trying to cover all my bases to avoid duplicate declarations of macros and writing the safest code possible. Any and all input and recommendations are appreciated and considered.

IO.h

#ifndef IO_H
#define IO_H

#ifdef IO_IMPORT_ALL
    #define PRINT
    #define INPUT
    #define TYPE_FINDER
#endif

#ifdef PRINT

    #include <tgmath.h>
    #include <stdio.h>

    #if !defined(TYPE_FINDER) || !defined(INPUT)
        #define TYPE_FINDER
    #endif
    #ifdef PRINT_NEWLINE
        #define print(value) do {               \
            printf(type_finder(value), value);  \
            printf("\n");                       \
        } while (0)
    #else
        #define print(value) printf(type_finder(value), value)
    #endif

#endif

#ifdef INPUT

    #include <stdio.h>
    #include <tgmath.h>

    #if !defined(TYPE_FINDER) || !defined(PRINT)
        #define TYPE_FINDER
    #endif
    #define input(prompt, variable) do {         \
        printf("%s", prompt);                    \
        scanf(type_finder(variable), &variable); \
    } while (0)

#endif

#ifdef TYPE_FINDER

    #define type_finder(type) _Generic((type),  \
        char:           "%c",                   \
        int:            "%d",                   \
        float:          "%f",                   \
        unsigned int:   "%u",                   \
        char*:          "%s",                   \
        void*:          "%p"                    \
    )

#endif

#endif // IO_H

main.c

#define IO_IMPORT_ALL
#define PRINT_NEWLINE
#include "IO.h"

void PrintTest(void) {

    print((char)'c');
    print(10);
    print(24.7f);
    print((unsigned int)24);
    print("test");
    print((void*)"abcde");

}

void InputTest(void) {

    int number;
    input("Enter a number: ", number);
    print(number);

}

int main() {

    PrintTest();
    InputTest();

    return 0;

}

I'm specifically tagging C11 because I need to use _Generic to distinguish the types passed to the macros.

If you want to run this code, here's my run.sh

gcc main.c -o main
./main
rm main
\$\endgroup\$
1
  • 1
    \$\begingroup\$ I don't think the C11 tag is necessary for code that works fine with current standard C (i.e. C17). \$\endgroup\$ Commented Nov 26, 2021 at 13:56

4 Answers 4

2
\$\begingroup\$

This looks like a missed opportunity. I was hoping to see something that would usefully handle re-trying when the input can't be parsed, but instead we have a macro that throws away the return value from scanf(), so that we have no idea whether it was successful or not.

I'm not a fan of headers that change their behaviour according to what macros are earlier defined. This is especially worrying as only the macros that are defined the first time it's included will have any effect. I'm not sure that is a successful idea - if the namespace pollution is a problem, it's probably easier to #include the header and then #undef the problematic names.

Talking of names, it's usual to use ALL_CAPS for macros - particularly ones like these, which expand arguments more than once. Macros don't behave like functions, and it's useful to be warned.

This looks surprising:

#if !defined(TYPE_FINDER) || !defined(INPUT)
    #define TYPE_FINDER
#endif

Why does defined(INPUT) need to be considered?

I'd simplify further - just unconditionally define it:

    #undef TYPE_FINDER
    #define TYPE_FINDER
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the review! I totally overlooked error handling when scanf fails. I went about the header to reduce compile time if the user only wants to use a specific function in the header. That may be ignorant, but I figured it could provide some performance improvements. I used print and input not using all caps because it just looks like a simple function call, which is what I was trying to accomplish. Thanks for pointing out the convolution in the multiple #defines with type_finder! It looked messy, and I'm glad another person agreed with me and provided some solutions. \$\endgroup\$
    – Linny
    Commented Nov 26, 2021 at 18:10
  • \$\begingroup\$ I think that any difference in compile time is likely to be imperceptible. But if you care about it, don't just accept my guess - measure! And remember that debugging time is more expensive than compilation time... \$\endgroup\$ Commented Nov 30, 2021 at 8:26
3
\$\begingroup\$

I would like to know if this is the best way to go about this

Over all: Good journeyman effort for printing. Unusable for reading.


Non-C syntax

Code looks wrong as it appears that input() takes 2 arguments: a char * and an (uninitialized) int. Instead, via the underlying macro, it is the & number that is used. I suspect C++ influence here.

int number;
input("Enter a number: ", number);

I'd expect the below and changes in the macro to support it.

input("Enter a number: ", &number);

Input Woes

To input a char and function like the others, I'd expect " %c" rather than "%c".

Much is missing from input

  1. Error handing - how to convey input is bad?

  2. stdin recovery - what is the state of stdin with bad input? Is all the input line read? Consider challenging input for an int like "123xyz", "123.0", "\n", "123456789012345678901234567890", "0x123", ...

  3. fgets() is much more robust than scanf(). This input() leaves the trailing '\n' in stdin, like typical scanf() usage, discouraging use with the good fgets().

Input Woes 2

Try below. I get format '%s' expects argument of type 'char *', but argument 2 has type 'char (*)[80]' [-Wformat=]

char number[80];
input("Enter text: ", number);

Input Woes 3

Passing a char * to input() results in a char ** passed to scanf("%s", ...) - mis-matched type leads to UB.

warning: format '%s' expects argument of type 'char *', but argument 2 has type 'char **' [-Wformat=]

Input strings

"%s" is not for reading strings. At best scanf("%s", ... reads a word (text input without white-space). Consider

char *name = malloc(80);
input('Enter full name", name);

This is worse than gets with no input limit, inability to read and save spaces and leaves '\n' in stdin.

Competing goals

"Simplified print and input" or a header only solution?

Use of macros loses type checking in the prompt and loss of return values.

If a "simplified print and input" can be made better with a IO.c, I suspect it can, do not constrain the code to an IO.h only solution.

double ?

Generically printing floating point with "%f" may be safe, yet it is not satisfactory with much of the range. Large float values have potentially dozens of uninformative digits and 40% of all float (the small ones) print "0.000000" or "-0.000000". "%f" also does not distinctively print many different float values - use more precision.

Instead keep the floating in floating point. In the _Generic_ for output:

    float:          "%.9g",                   \
    double:         "%.17g",                   \

... or other code that drives the 9,17 from FLT_DECIMAL_DIG, DBL_DECIMAL_DIG.

Avoid casts

// print((unsigned int)24);
print(24u);

Missing types

Certainly code is a concept, but to note there are many other types

  • signed char, unsigned char
  • short, unsigned char
  • long, unsigned long
  • long long, unsigned long long
  • double, long double
  • complex float, complex double, complex long double

Types like size_t, uint64_t, intmax_t, intptr_t, wchar_t, etc. may or may not have been already listed above. It takes creative use of _Generic to handle these.

Name space

Including io.h surprisingly defines print and input. These very popular names can likely collide with other code. Perhaps io_print, io_input instead?

Lighter code

// printf("%s", prompt);
fputs(prompt, stdout);

See also: Formatted print without the need to specify type matching specifiers using _Generic

\$\endgroup\$
0
3
\$\begingroup\$

Naming:

    #define type_finder(type) _Generic((type),  \
        char:           "%c",                   \
        int:            "%d",                   \
        float:          "%f",                   \
        unsigned int:   "%u",                   \
        char*:          "%s",                   \
        void*:          "%p"                    \
    )

What you've named type is an expression, or expr.

From cppreference:

_Generic ( controlling-expression , association-list ) (since C11)

...

where

controlling-expression - any expression (except for the comma operator) whose type must be compatible with one of the type-names if the default association is not used.


An implementation that covers all types (minux exact-width integer types (uint8_t, int8_t et cetera), minimum-width integer types (uint_least8_t, int_least8_t, et cetera), fastest minimum-width integer types (uint_fast8_t, uint_fast8_t et cetera), inptr_t, uintptr_t, intmax_t, uintmax_t, et cetera):

/* Get the corresponding format specifier of a type. */
#define PRINTF_FMT(T)                        \
    _Generic((T),                            \
        _Bool                   : "%d",      \
        char                    : "%c",      \
        signed char             : "%hhd",    \
        unsigned char           : "%hhu",    \
        short int               : "%hd",     \
        int                     : "%d",      \
        long int                : "%ld",     \
        long long int           : "%lld",    \
        unsigned short int      : "%hu",     \
        unsigned int            : "%u",      \
        unsigned long int       : "%lu",     \
        unsigned long long int  : "%llu",    \
        float                   : "%.9g",    \
        double                  : "%.17g",   \
        long double             : "%Lf",     \
        char *                  : "%s",      \
        wchar_t *               : "%ls",     \
        void *                  : "%p"       \
    )

#define print(x)    printf(PRINTF_FMT(x), x)

/* A separate call to putchar() is required because _Generic is not a macro, but
 * a primary expression. As such it is evaluated at a later translation phase (7)
 * than string concatenation (phase 6). */
#define println(x)  printf(PRINTF_FMT(x), x), putchar('\n')

See the IS_SIGNED() type trait here General Purpose Utility Constants, Macros, and Functions to see how you can detect the other standard types with _Generic.

\$\endgroup\$
2
\$\begingroup\$

I found this very interesting and was not aware of the _Generic operation in C.

The following are really more questions than suggestions.

Is there any specific reason for including tgmath.h?

The do ... while(0) construct seems odd to me. It looked like you used to force the inner part into a scope of its own. Is there any specific reason why you could not have simply used:

#define print(value) {                          \
            printf(type_finder(value), value);  \
            printf("\n");                       \
        }

print only allows some basic types. It will result in an error if you try to print for example a double. Is this by design or just to simplify the example code?


I have tried to compile this with MSVC but it looks like its not happy with the _Generic operation. Most likely I'm just missing the correct build option.

Intel C compiler generates some warnings, mostly about it not liking scanf

note: 'scanf' has been explicitly marked deprecated here
\$\endgroup\$
7
  • 1
    \$\begingroup\$ Thanks for the review! While writing the header, I thought I needed tgmath.h to actually use _Generic. Running it again just now, I realized the opposite. The do-while was my way of having multi-line macros, but looking at your proposition, that looks a lot cleaner. The print function was written based on types that I needed to print specifically, but I probably should expand it to print all types of variables. I compiled this using gcc on a Mac, and again with -Wall enabled, and I didn't get that compiler warning. Thanks again! \$\endgroup\$
    – Linny
    Commented Nov 26, 2021 at 18:13
  • \$\begingroup\$ Concerning "tried to compile this with MSVC" --> Are you compiling with a version that is C99, C11 or C17 compatible or are you using a compiler that only handles pre-C99 (22+ year old) code? \$\endgroup\$ Commented Nov 27, 2021 at 13:28
  • 1
    \$\begingroup\$ The "'scanf' has been explicitly marked deprecated here" is not deprecated by the C spec, but by some other standard. \$\endgroup\$ Commented Nov 27, 2021 at 13:29
  • 1
    \$\begingroup\$ @chux-ReinstateMonica, yeah I missed the /std:c11 flag, compiles now with msvc except for the _CRT_SECURE_NO_WARNINGS crap. \$\endgroup\$
    – jdt
    Commented Nov 27, 2021 at 14:31
  • 2
    \$\begingroup\$ Regarding do-while(0) macros and why we should (not) use them, see: What is do { } while(0) in macros and should we use it? \$\endgroup\$
    – Lundin
    Commented Nov 29, 2021 at 10:23

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.