3
\$\begingroup\$

Re - designing c-style input output [ btw I use printf() and scanf() in the core ]

Reasons for redesigning it :-

  • Needs 2 functions for input output [ namely printf() and scanf() ]
  • Can't customize the format specifiers
  • Can't print float/double point precisely / dynamically [ i.e printing 3.5 makes 3.500000 from printf("%f",3.5);, we can use %.1f instead of %f but that's explicit and not dynamic and only works in that context ]
  • To get input with written text, We need to printf() then scanf()
  • Code is increased and looks a bit long [ espacially when we have to input lot of things from user ]

Improvements after redesigning it :- [ NOTE: I use printf() and scanf() in the core but it is code friendly at the end ]

  • Needs only 1 function for both input and output [ format specifier are different default: $ for output, % for input ]
  • Can customize the format specifiers
  • Can print float/double precisely / dynamically or if you want explicitly
  • We have only one function, so we can directly print text and get input
  • Code is reduced
  • Amazingly if you only want print, you can using print() which is included only for output and not for input

file: io.h

#ifndef IO_H_INCL
#define IO_H_INCL
#ifndef IO_OUTPUT_TOK
#define IO_OUTPUT_TOK '$'
#endif
#ifndef IO_INPUT_TOK
#define IO_INPUT_TOK '%'
#endif
#ifndef IO_DEF_STR_SPACE
#define IO_DEF_STR_SPACE 0
#endif

#include<stdarg.h>

void vio(const char*, char, va_list);
void print(const char*, ...);
void io(const char*, ...);

#endif

file: io.c

#include "io.h"
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

/* ---- Format Specifier ---- [ default ]
change the '$' if you have customized it!

 ==  ==  For Output  ==  == 
    - $i : integer
    - $c : charachter
    - $f : float
    - $d : double
    - $.nf : prints n number of points after main value [ float ] ( n needs to be 0-9 )
    - $.nd : prints n number of points after main value [ double ] ( n needs to be 0-9 )
    - $u : unsigned integer
    - $p : pointers
    - $x : hexadecimal conversion from int
    - $li : long
    - $lu : long unsigned
    - $ll : long long
    - $LL : long long unsigned
    - $s : string

change the '%' if you have customized it!
 ==  ==  For input  ==  == 
    - %i : integer
    - %c : charachter
    - %f : float
    - %d : double
    - %u : unsigned integer
    - %li : long int
    - %lu : long unsigned [int]
    - %ll : long long [int]
    - %lL : long long unsigned [int]
    - %s : string
*/

// main input output
void vio(const char* fmt, char mode, va_list va){
    size_t i;
    for (i = 0; i < strlen(fmt); i++)
    {
        char cch = fmt[i];
        // if mode == 0 [means input and output both]
        if (!mode)
        {
        // if input token is the current charachter
        if (cch == IO_INPUT_TOK){
            char cch2 = fmt[i+1];
            i+=1;
            // process based on input format specifier
            if (cch2 == 'i' || cch2 == 'I'){
                int* iptr = va_arg(va,int*);
                scanf("%d",iptr);
            }else if (cch2 == 'c' || cch2 == 'C'){
                char* cptr = va_arg(va,char*);
                scanf("%c",cptr);
            }else if (cch2 == 'f' || cch2 == 'F'){
                float* fptr = va_arg(va,float*);
                scanf("%f",fptr);
            }else if (cch2 == 'd' || cch2 == 'D'){
                double* dptr = va_arg(va,double*);
                scanf("%lf",dptr);
            }else if (cch2 == 'u'){
                unsigned int* uptr = va_arg(va,unsigned int*);
                scanf("%u",uptr);
            }else if (cch2 == 'l' || cch2 == 'L'){
                char cch3 = fmt[i];
                i+=1;
                if (cch3 == 'i' || cch3 == 'I')
                {
                    long int* li = va_arg(va,long int*);
                    scanf("%li",li);
                }else if (cch3 == 'l'){
                    long long int* l = va_arg(va,long long int*);
                    scanf("%lli",l);
                }else if (cch3 == 'L'){
                    long long unsigned int* l = va_arg(va,long long unsigned int*);
                    scanf("%llu",l);
                }else if (cch3 == 'u'){
                    long unsigned int* lu = va_arg(va,long unsigned int*);
                    scanf("%lu",lu);
                }else{
                    i-=1;
                    printf("%c%c%c",IO_INPUT_TOK,cch2,cch3);
                }
            }else if (cch2 == 's'){
                // if string check if string is affected by space
                // if not [as default] make up the whole input to string
                char* s = va_arg(va,char*);
                if (IO_DEF_STR_SPACE){
                    scanf("%s",s);
                }else{
                    scanf("%[^\n]s",s);
                }
            // if it's like %% or INPUT_TOK INPUT_TOK just print INPUT_TOK
            }else if (cch2 == IO_INPUT_TOK){
                printf("%c",IO_INPUT_TOK);
                i-=1;
            }
        }
        }
        // check for output token
        if (cch == IO_OUTPUT_TOK){
            char cch2 = fmt[i+1];
            i+=1;
            // everything is pretty simple
            // check what format specifier, printf it
            if (cch2 == 'i' || cch2 == 'I'){
                printf("%d",va_arg(va,int));
            }else if (cch2 == 'c' || cch2 == 'C'){
                printf("%c",va_arg(va,int));
            }else if (cch2 == 'f' || cch2 == 'F'){
                // float print dynamic points!
                // make a string to store result of snprintf, i for loop variable,
                //flt for actual float value, s2 for output string
                
                char s[50]="";
                char s2[50]="";
                size_t i;
                float flt = va_arg(va,double);

                snprintf(s,50,"%f",flt);
                // states that the current isZero is true [ default ]
                int isZero = 1;

                for(i=strlen(s); i>0; i--)
                {
                    // if previously it was zero and now its not, make it false!~
                    // stripping zero mechanism [ stripps the continuous zero till hitting
                    // a non-zero value ]
                    if (isZero&&s[i-1] != '0'){
                        isZero = 0;
                    }

                    // if the zeroes are already stripped out or it is the last element,
                    // store it in output s2
                    if (!isZero || s[i-2] == '.'){
                        s2[i-1] = s[i-1];
                    }
                }
                // print the output str
                printf("%s",s2);

            }else if (cch2 == 'd' || cch2 == 'D'){
                // double print dynamic points!
                // make a string to store result of snprintf, i for loop variable,
                //flt for actual float value, s2 for output string
                
                char s[50]="";
                char s2[50]="";
                size_t i;
                double dbl = va_arg(va,double);

                snprintf(s,50,"%lf",dbl);
                // states that the current isZero is true [ default ]
                int isZero = 1;

                for(i=strlen(s); i>0; i--)
                {
                    // if previously it was zero and now its not, make it false!~
                    // stripping zero mechanism [ stripps the continuous zero till hitting
                    // a non-zero value ]
                    if (isZero&&s[i-1] != '0'){
                        isZero = 0;
                    }

                    // if the zeroes are already stripped out or it is the last element,
                    // store it in output s2
                    if (!isZero || s[i-2] == '.'){
                        s2[i-1] = s[i-1];
                    }
                }
                // print the output str
                printf("%s",s2);

            }else if (cch2 == '.'){
                // if its like $.2f or something explicit like $.2d for double
                char cch3 = fmt[i+1];
                // check if the next number is not a number [ we want it to be a number]
                if (cch3>'9' || cch3<'0'){
                    printf("%c.",IO_INPUT_TOK);
                }else{
                    // if it is a number check that the last one is a 'f' or a 'd'
                    char cch4 = fmt[i+2];
                    i+=2;
                    if (cch4 == 'f')
                    {
                        // preparation string for float
                        char mk[] = "%. f";
                        // since the number is in cch3 [ the explicitly point printing num ]
                        // assign it so it becomes like %.nf where n is the number
                        mk[2] = cch3;

                        // use the preparation string to print what's in the argument
                        printf(mk,va_arg(va,double));

                    }else if (cch4 == 'd'){
                        // preparation string for double
                        char mk[] = "%. lf";
                        // since the number is in cch3 [ the explicitly point printing num ]
                        // assign it so it becomes like %.nlf where n is the number
                        mk[2] = cch3;

                        // use the preparation string to print what's in the argument
                        printf(mk,va_arg(va,double));
                        
                    }else{
                        i-=1;
                        printf("%c.%c%c",IO_OUTPUT_TOK,cch3,cch4);
                    }
                }
            // just check and print it
            }else if (cch2 == 'u' || cch2 == 'U'){
                printf("%u",va_arg(va,unsigned int));
            }else if (cch2 == 'p' || cch2 == 'P'){
                printf("%p",va_arg(va,void*));
            }else if (cch2 == 'x' || cch2 == 'X'){
                printf("%x",va_arg(va,int));
            }else if (cch2 == 's' || cch2 == 'S'){
                printf("%s",va_arg(va,char*));
            }else if (cch2 == 'l' || cch2 == 'L'){
                // if it is in long type
                char cch3 = fmt[i];
                i+=2;
                if (cch3 == 'i' || cch3 == 'I'){
                    printf("%li",va_arg(va,long int));
                }else if (cch3 == 'l'){
                    printf("%lli",va_arg(va,long long int));
                }else if (cch3 == 'L'){
                    printf("%llu",va_arg(va,long long unsigned int));
                }else if (cch3 == 'u'){
                    printf("%lu",va_arg(va,long unsigned int));
                }else{
                    i-=1;
                    printf("%c%c%c",IO_OUTPUT_TOK,cch2,cch3);
                }
            }else if (cch2 == IO_OUTPUT_TOK){
                printf("%c",IO_OUTPUT_TOK);
            }else{
                i-=1;
                printf("%c",IO_OUTPUT_TOK);
            }
        // if its not input token or if the mode states its not input mode,
        // print the charachter whatever it may be
        }else if (cch!=IO_INPUT_TOK||mode){
            printf("%c",cch);
        }
    }
}

// full wrapper for vio
void io(const char* fmt, ...)
{
    va_list va;
    va_start(va,fmt);
    vio(fmt,0,va);
    va_end(va);
}

// print "only" wrapper for vio
void print(const char* fmt, ...)
{
    va_list va;
    va_start(va,fmt);
    vio(fmt,1,va);
    va_end(va); 
}

file: exampleUsage.c

// format specifiers to be customized before including io.h

// #define IO_INPUT_TOK '%' // uncomment and change the '%' to your own token
// #define IO_OUTPUT_TOK '$' // uncomment and change the '$' to your own token

#include "io.h"

/* ---- Format Specifiers ---- [ default ]
change the '$' if you have customized it!

 ==  ==  For Output  ==  == 
    - $i : integer
    - $c : charachter
    - $f : float
    - $d : double
    - $.nf : prints n number of points after main value [ float ] ( n needs to be 0-9 )
    - $.nd : prints n number of points after main value [ double ] ( n needs to be 0-9 )
    - $u : unsigned integer
    - $p : pointers
    - $x : hexadecimal conversion from int
    - $li : long
    - $lu : long unsigned
    - $ll : long long
    - $LL : long long unsigned
    - $s : string

change the '%' if you have customized it!
 ==  ==  For input  ==  == 
    - %i : integer
    - %c : charachter
    - %f : float
    - %d : double
    - %u : unsigned integer
    - %li : long int
    - %lu : long unsigned [int]
    - %ll : long long [int]
    - %lL : long long unsigned [int]
    - %s : string
*/

int main()
{
    double a, b;
    // $i maps to -> 1, %d maps to -> &a, $i maps to -> 2, %d maps to -> &b
    io("Num $i: %dNum $i: %d", 1, &a, 2, &b);
    // $d maps to -> a+b
    io("Sum is: $d\n",a+b); //or print("Sum is : $d\n",a+b);
    return 0;
}

I would be happy to receive suggestions, feedback, improvements, and bugs of my code. I have used it and it works with no errors or warnings

gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0 using flags -Wall -Wextra -Werror

\$\endgroup\$
6
  • \$\begingroup\$ int Zero = 1; -- are you serious that zero equals one? Sounds strange to me. \$\endgroup\$ Jun 21, 2022 at 23:07
  • \$\begingroup\$ Your code looks very condensed, which makes it hard to read for most C programmers. The usual style is to have many more spaces, especially around binary operators and around braces. As long as there are no answers yet, you are free to modify your code to make it more pleasant for humans to read it. \$\endgroup\$ Jun 21, 2022 at 23:09
  • \$\begingroup\$ Ok btw the int Zero = 1 means that there is no zero is snprintf'ed string str \$\endgroup\$
    – Pear
    Jun 22, 2022 at 1:59
  • \$\begingroup\$ I will edit it later ( i have class now ) thanx for the feedback \$\endgroup\$
    – Pear
    Jun 22, 2022 at 1:59
  • 1
    \$\begingroup\$ The comment by Roland above was not asking for an explanation; but rather pointing out that your variable names are a bit "iffy". Naming variables is an important skill. The hardest part of coding is not writting the code (you understand it now), it is coming back to it in 6 months after you have forgotten all the context, or reading somebody else's code. Good naming of variables and function will help you understand the code much more easily in these situations. \$\endgroup\$ Jun 22, 2022 at 17:56

2 Answers 2

3
\$\begingroup\$

So here we go. As usual, be prepared that the feedback here is quite detailed.

The first thing I did was to load your code into my IDE and run gcc -O2 -Wall -Wextra -Werror on it. The -O2 is needed because without this optimization level, some consistency checks are skipped. There were no warnings indeed, just as you promised. 👍

The next step was to check your code into version control, so that I could do any changes without losing the original code.

After setting up the backup, I was ready to format your code to my likings. This changed a+b to a + b and if(cond){ to if (cond) {, as I'm more used to that standard layout of C code.

Now to the API from io.h:

  • The idea of having input and output intermixed in a single function call is appealing for interactive programs that follow this input-output style that originated in the 1970, when line printers were a common thing.
  • I don't think that style of input-output is common nowadays, except for some introductory programming courses that just take their material from wherever they can get it, without questioning whether it still applies to reality.
  • Using varargs for input/output is dangerous, as the compiler will not tell you about accidental typos. The code in GCC that validates format strings is 5000 lines long. To make your API similarly safe to use, you would have to write this much support code for the common compilers, or for a separate iolint tool.
  • I don't see why anyone would need the flexibility of configuring the input and output characters. Environments that support <stdio.h> usually can handle the '$' character.

Now to the implementation in io.c:

  • Putting the detailed documentation at the beginning of the file is great. It could use a little more formatting, to align the columns of the (invisible) tables vertically, but aside from that, the documentation covers the basic API usage. Talking about API usage, the documentation should probably go into the header file, not the implementation file.
  • There's a typo in the word for 'character', says my IDE.
  • The output specifiers are not sorted in any meaningful way. The usual way would be integers first (from small to big), then floating point, then everything else.
  • Why are the decimal places of floating points limited to 0-9? The IEEE 754-1985 64-bit floating point type has 17 significant decimal places, and I want to be able to read and write all of them.
  • The input section differs unnecessarily from the output section:
    • $li means long, %li means long int. Either use long or long int, don't mix them arbitrarily.
    • $LL means long long unsigned. This type is usually spelled unsigned long long. There is no clue in $LL that this means unsigned. The printf family of functions did this better by using %llu, which has a 1:1 correspondence to unsigned long long.
    • Why do you use uppercase letters at all, instead of repeating the lowercase l?

Regarding the function vio:

  • Is the parameter mode intended to either be IO_OUTPUT_TOK or IO_INPUT_TOK? If so, the name of the parameter should match the names of the constants. I don't see how TOK relates to mode.
  • Either way, the documentation for this parameter is missing.
  • Instead of #define, it is easier to define an enum, which gives you a bit more type safety, depending on the compiler and the compiler options. It definitely helps humans to understand the code.
  • Since mode has type char, don't use boolean operators on it. Instead of !mode, write mode == '\0', which makes the comment above that line partly redundant.
  • My IDE yells at me that each call to scanf is missing the error handling, and indeed, when I ran your example program and entered three for the first number, your program said: Num 2: Sum is: 0.0, which is nonsense. Your example program should demonstrate how to use your code in a robust way.
  • Why do you handle uppercase and lowercase format specifiers in the same way? The documentation says nothing about that, and it makes the code harder to read. Just using lowercase characters is enough.
  • Why is u only allowed in lowercase? That's inconsistent.
  • I didn't thoroughly check all the branches of that huge loop, that's the job of your test suite. Speaking of which, you didn't provide one. This means that you cannot be sure that after tweaking your code, all the basic use cases still work as intended.
  • Your code produces buffer overflows when reading a string. It is as bad as the function gets that was banned from the C standard library because it is impossible to be used in a safe way.
  • For int isZero = 1;, you are using the wrong data type. If that variable is supposed to be a boolean variable, make its type bool instead of int. The comment above that declaration is useless, as it only states the obvious.
  • The code for printing float and double is so large that you should extract it to a separate function. Maybe you don't even need the same code twice; the two cases for float and double look quite similar after all.

Regarding your example usage program:

  • The program is too short. It doesn't cover 10% of your actual code.

Regarding your automated tests:

  • They are missing.

Summary:

  • I wouldn't use your code, and if I would find it in a larger project, I would throw it away and migrate the code to using the C standard library.
  • While the C standard library has its own problems in terms of robustness, error handling and buffer overflows, these are well-known and the most obvious bugs are caught by compilers or linters.
  • The scanf family of functions has built-in error handling, your code ignores all errors and doesn't provide any way to let the caller deal with them. This makes your code only suitable for unreliable programs.
\$\endgroup\$
2
  • \$\begingroup\$ I totally agree with you but I couldn't understand what you meant by "automated tests" ? \$\endgroup\$
    – Pear
    Jun 23, 2022 at 1:35
  • \$\begingroup\$ Automated tests are those that require no interactivity, such as unit tests. \$\endgroup\$ Jun 24, 2022 at 0:13
2
\$\begingroup\$

I agree with Roland Illig's review. Some other remarks I would like to add:

Needs 2 functions for input output [ namely printf() and scanf() ]

I don't think this is a big problem. Merging a lot of things into one big function can make code more complicated. Also, a lot of people already are used to the standard library, and I don't think many would switch to your way of doing I/O unless there is a huge benefit (and not just removing trailing zeroes from numbers). If this is for your personal use, then it's fine of course, but if you want to use it in projects that you want to collaborate on with others, this could possibly introduce some friction.

Can't customize the format specifiers

Some standard libraries do allow you to customize format specifiers. Of course that might be less portable than your code.

Can't print float/double point precisely / dynamically [ i.e printing 3.5 makes 3.500000 from printf("%f",3.5);, we can use %.1f instead of %f but that's explicit and not dynamic and only works in that context ]

There is %g which might be closer to what you want. Another issue is that while you add this feature of dynamically removing trailing zeroes for floating point values, you did not implement a whole lot of other features that come standard with the printf() family of functions.

Unless you are prepared to fix all the issues mentioned by Roland, and making this a strict superset of all the features provided by scanf() and printf(), then I would recommend that you should not want to do this at all.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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