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
-
1\$\begingroup\$ I don't think the C11 tag is necessary for code that works fine with current standard C (i.e. C17). \$\endgroup\$Toby Speight– Toby Speight2021年11月26日 13:56:23 +00:00Commented Nov 26, 2021 at 13:56
4 Answers 4
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
-
\$\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 usedprint
andinput
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
withtype_finder
! It looked messy, and I'm glad another person agreed with me and provided some solutions. \$\endgroup\$Ben A– Ben A2021年11月26日 18:10:12 +00:00Commented 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\$Toby Speight– Toby Speight2021年11月30日 08:26:47 +00:00Commented Nov 30, 2021 at 8:26
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
Error handing - how to convey input is bad?
stdin
recovery - what is the state ofstdin
with bad input? Is all the input line read? Consider challenging input for anint
like"123xyz"
,"123.0"
,"\n"
,"123456789012345678901234567890"
,"0x123"
, ...fgets()
is much more robust thanscanf()
. Thisinput()
leaves the trailing'\n'
instdin
, like typicalscanf()
usage, discouraging use with the goodfgets()
.
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
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
.
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
-
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. Thedo-while
was my way of having multi-line macros, but looking at your proposition, that looks a lot cleaner. Theprint
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 usinggcc
on a Mac, and again with-Wall
enabled, and I didn't get that compiler warning. Thanks again! \$\endgroup\$Ben A– Ben A2021年11月26日 18:13:22 +00:00Commented 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\$chux– chux2021年11月27日 13:28:07 +00:00Commented 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\$chux– chux2021年11月27日 13:29:37 +00:00Commented 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– jdt2021年11月27日 14:31:03 +00:00Commented 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– Lundin2021年11月29日 10:23:41 +00:00Commented Nov 29, 2021 at 10:23