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
  • \$\begingroup\$ I don't think the C11 tag is necessary for code that works fine with current standard C (i.e. C17). \$\endgroup\$ Nov 26 '21 at 13:56
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
    Nov 26 '21 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\$ Nov 30 '21 at 8:26
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\$
6
  • 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
    Nov 26 '21 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\$ Nov 27 '21 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\$ Nov 27 '21 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
    Nov 27 '21 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
    Nov 29 '21 at 10:23
2
\$\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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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