Search code examples
crangeoperators

Are there range operators in the C programming language?


Are there range operators in C like there are in, say, Swift (closed range, open range)?
For example, in a switch statement, in Swift I would write this:

switch num {
    case 144...168:
    print("Something between 144 and 168")

    case 120..<144:
    print("Something between 120 and 143")
    
    default: break
}

If I try something similar in C, using printf instead of print of course, I cannot use the same range operators. Is there something I can use instead?
Thank you


Solution

  • You can use a series of if statements.

    When I do range matches, I create macros to simplify the code.

    And, I've found that a do { } while (0); block can simplify things vs. a large if/else ladder.

    Here is some code:

    #define RNGE(_val,_lo,_hi) \
        (((_val) >= (_lo)) && ((_val) <= (_hi)))
    #define RNGEM1(_val,_lo,_hi) \
        (((_val) >= (_lo)) && ((_val) < (_hi)))
    
    do {
        if (RNGE(num,144,168)) {
            printf("Something between 144 and 168\n");
            break;
        }
    
        if (RNGEM1(num,120,144)) {
            printf("Something between 120 and 143\n")
            break;
        }
    
        // default ...
    } while (0);
    

    UPDATE:

    Nice! Not too familiar with macros, but they look like functions. What is the backslash at the end of the line? Also, why the underscores? Convention? Preference? Second and fourth line perfectly clear! – NotationMaster

    Macros are a way to "generate" code. They are [usually] processed in a separate first stage of the compiler: the C preprocessor (aka cpp). They operate as if you "inserted" the code with an editor.

    The backslashes are required if you want to define your macro on multiple lines [as I did].

    We can look at the source generated by the macros with (e.g.)

    cc -E -P -o foo.i foo.c
    

    Here is the output of that stage:

    do {
     if ((((num) >= (144)) && ((num) <= (168)))) {
      printf("Something between 144 and 168\n");
      break;
     }
     if ((((num) >= (120)) && ((num) < (144)))) {
      printf("Something between 120 and 143\n")
      break;
     }
    } while (0);
    

    The underscores are my personal convention (developed after 40+ years ;-).

    Most people would define the macros as:

    #define RNGE(num,lo,hi) \
        (((num) >= (lo)) && ((num) <= (hi)))
    #define RNGEM1(num,lo,hi) \
        (((num) >= (lo)) && ((num) < (hi)))
    

    I use them to distinguish the macro arguments from C symbols. Notice here I changed _val into num. I did that deliberately to use the same symbol your example used (i.e. num). Now, the generated output has num in it. But, is that num from the [function] scoped C variable directly (e.g. I forgot to define the argument in the macro) or did it come through the argument?

    This are somewhat controversial because, in C, there are standards for libraries (e.g. POSIX) that reserve all symbols that start with "_" for standard library usage.

    That is just a convention, however. It's rare for a symbol I define with "_" to conflict with any internal library symbol. If one ever does, I'm perfectly willing to accept the responsibility to change my code. In fact, in 40 years, I've never had such a conflict and have never had to rework code.

    For macro arguments, they are in a different namespace, so far less likely to conflict with a global variable or function name in a [POSIX] library.

    Also, I consider it to be extreme hubris on the part of POSIX to co-opt such a broad mechanism for themselves. In python, an "_" prefix is a convention that the object function is private to the object.

    If you want the benefit of the "_" without the hassle of "violating" the convention (e.g):

    #define RNGE(val_,lo_,hi_) \
        (((val_) >= (lo_)) && ((val_) <= (hi_)))
    #define RNGEM1(val_,lo_,hi_) \
        (((val_) >= (lo_)) && ((val_) < (hi_)))
    

    Macros are useful in certain situations where a function won't work. Consider the following where we replace the macro with an inline function:

    static inline int
    RNGE(int val,int lo,int hi)
    {
        return (val >= lo) && (val <= hi);
    }
    
    // this works ...
    int num;
    do {
        if (RNGE(num,144,168)) {
            printf("Something between 144 and 168\n");
            break;
        }
    
        // default ...
    } while (0)
    
    // this blows up at compile time ...
    int arr[1000];
    int *ptr = &arr[37];
    do {
        if (RNGE(ptr,&arr[144],&arr[168])) {
            printf("Something between array elements 144 and 168\n");
            break;
        }
    
        // default ...
    } while (0)
    

    C doesn't have overloaded functions like some languages (e.g. C++). So, we have to explicitly generate separate functions for each argument type.

    In this instance, the macro works for any type that can be compared.


    UPDATE #2:

    For more on using do { ... } while (0) in place of if/else ladder logic or switch/case see my answer: How to properly make a counting algorithm to count from file?

    thank you for the extra explanation. I recall in Swift these range operators would be perfect for usage in a switch statement. Yet, here, if I use this macro in the switch, the compiler still tells "Expression is not an integer constant expression". Am I missing something here? – NotationMaster

    The short answer is ...

    1. with switch (switch_expression) then switch_expression can be just about anything.

    2. But, a limitation of C with case case_expression is that case_expression can only be something that evaluates to a constant at compile time.

    3. switch/case is a [very useful] nicety. But, anything it does can be done with if/else.

    4. So, the way to handle this is to use the code in my first example above.

    Warning: This is where you should probably stop reading ... :-)

    The long answer ...

    So, why does C have this restriction? Mostly historical. But, C wants to generate extremely fast code.

    Many switch/case blocks use consecutive numbers in the case expressions:

    switch (num) {
    case 1:
        x = 23;
        break;
    
    case 2:
        y = 37;
        break;
    
    case 3:
        z = 19;
        break;
    
    case ...:
        ...;
        break;
    
    case 100:
        q = 28;
        break;
    
    default:
        abort();
        break;
    }
    

    The simple/obvious code would be (using if/else ladder logic that I convert here to use my do/while/0 "trick" would be:

    do {
        if (num == 1) {
            x = 23;
            break;
        }
    
        if (num ==  2) {
            y = 37;
            break;
        }
    
        if (num == 3) {
            z = 19;
            break;
        }
    
        // cases for 4-99 ...
    
        if (num == 100) {
            q = 28;
            break;
        }
    
        // default:
        abort();
    } while (0);
    

    However ... There's a lot of if statements in this. But, because the case expressions are consecutive, the optimizing compiler can detect this and generate [asm] code that does a range check (i.e. if num is within the range 1-100) and can use the num value to index into a pointer table that jumps directly to the corresponding code snippet.

    Here is some pseudo code that shows this:

    static void *switch_vector[] = {
        &&L_1,
        &&L_2,
        &&L_3,
        ...,
        &&L_100
    };
    
    if (RNGE(num,1,100))
        goto switch_vector[num - 1];
    else
        goto L_DEFAULT;
    
    L_1:
        x = 23;
        goto L_ENDSWITCH;
    
    L_2:
        y = 37;
        goto L_ENDSWITCH;
    
    L_3:
        z = 19;
        goto L_ENDSWITCH;
    
    // more labels ...
    
    L_100:
        q = 28;
        goto L_ENDSWITCH;
    
    L_DEFAULT:
        abort();
    
    L_ENDSWITCH:
        // code after switch
    

    Side note: I called this "pseudo" code, but this is actually valid C code using the very advanced usage of a "computed" goto in C. Don't actually use it as it's virtually never needed. The compiler will usually generate much better code using the simpler constructs above. Remember, I did warn you :-)

    See:

    1. Can you make a computed goto in C++?
    2. https://eli.thegreenplace.net/2012/07/12/computed-goto-for-efficient-dispatch-tables