martinkunev
martinkunev

Reputation: 1405

Non-traditional uses of switch statement

Recently I found out that the body of a switch can be any statement (C99 6.8.4). The idea was first suggested to me by this: https://stackoverflow.com/a/9220598/515212

So one can have switch statements like

void f(int n)
{
    switch (n)
    case 0:
        printf("zero\n");
}

or even put ifs, whiles, etc.

void f(int n)
{
    switch (n) 
    if (1)
    {
        case 0:
            printf("zero\n");
    }
    else
        while (--n)
        {
            default:
                printf("non-zero\n");
        }
}

Just out of interest, I was wondering whether this syntax has some usage or is just an artifact of how the switch statement is defined in the standard?

Upvotes: 3

Views: 213

Answers (4)

Richard Chambers
Richard Chambers

Reputation: 17613

Here are a few unusual usages of switch() and case that I was playing around with this evening just to see how far it would stretch when I came across this question. Most of these come from various SO postings and answers.

The most famous unusual example is Duff's Device which is already covered in other answers to this question.

The basics of switch()

The switch() statement has the basic form of:

switch (<expression>) <statement>;

where

  • <expression> is an expression that evaluates to an integral value
  • <statement> is a statement or compound statement with labels

<statement> is either a single statement terminated by a semicolon or a series of statements enclosed in curly braces, a compound statement. Optional case labels which specify a constant value can be used to act as jump targets if the constant value equals the value of <expression> specified in the switch().

The case labels are similar to goto labels with a similar syntax and the switch() statement can be thought of as a computed goto statement in which the value of <expression> determines to which label within the scope of the switch() execution will jump.

The case labels must specify an integral constant value or an expression from which the compiler can create an integral constant value. Each value specified in a case label must be unique within the scope of the switch() or the special label of default can be used to specify any value other than the specified case label values. The order of the case labels does not matter.

The case and default labels do not need to be specified however switch(i) return 0; will not do a return since there are no labels to jump to. On the other hand switch(i) default: return 0; will always return no matter what the value of the variable i.

So the following are good switch() statements:

switch (i) case 1: case 2: return i;

switch (i) {
case 3:
   i++;
    break;

case 1:
    // FALL THROUGH
case 2:
    return i;

default:
    i -= ((i < 10) ? 2 : 5);
    break;
}

// an enum is an integral type so its possible to use them with switch()
typedef enum { TYPE_1 = 0, TYPE_2, TYPE_3 } MyTypes;

MyTypes jjkk = TYPE_1;
switch (jjkk) {
case TYPE_1: doType1(22); break;
case TYPE_2: doType2(33); break;
}

// you can use #define constants so long as they evaluate to an integral valued constant
// the Preprocessor will do the text substitution for you.
#define JKJK1   1
#define JKJK2   2

switch (i) case JKJK1: case JKJK2: printf("JKJK1 or JKJK2\n");


// following use of logical express should transform logical true/false result
// into an integral value of 1 (true) or 0 (false). Expect a warning about
// this usage though.
switch (i < 4) case 1: i *= 2;   // logical true evaluated as integral value of 1

Examples of switch() usage

The first example uses a do {} while(0) so that several of the cases use the same printf() statement but the cases prepare case specific data to be used in the output.

Assume that we have a data structure in a point of sale which contains a foreign currency tender, e.g. the customer is at an airport shop in Paris where the Euro is the standard currency but the shop also accepts US dollars and the point of sale does a conversion of US dollars to Euros. In the receipt printing we want to print just Euros if the customer is using Euros and if the customer is using US dollars then we want to print the amount of US dollars, the conversion rate, and the amount of Euros.

We have a struct containing the tender data that looks something like:

typedef struct {
    unsigned char   uchMajorCode;
    unsigned char   uchMinorCode;       // indicates which tender
    long    lTenderAmt;         // amount of tender in Euros
    long    lRate;              // conversion rate
    long    lForeignCurrency;   // amount of the foreign currency
    // .... other data
} ItemTender;

In the printing logic we have to determine how to format the receipt line for this tender. And yes I know there are other and more readable ways to do this.

switch (pItem->uchMinorCode) {
    char *mnemonic;
case FOREIGN_1: do {
        mnemonic = getMnemonic(TRN_FOREIGN_1);
        break;  // break from do {} while()
case FOREIGN_2:
        mnemonic = getMnemonic(TRN_FOREIGN_2);
        break;  // break from do {} while()
case FOREIGN_3:
        mnemonic = getMnemonic(TRN_FOREIGN_3);
        break;  // break from do {} while()
    } while(0);
    printf ("%s\t%ld  %ld @ %ld\n", mnemonic, pItem->lTenderAmt, pItem->lForeignCurrency, pItem->lRate);
    break;    // break from switch()
case LOCAL:
    // FALL THROUGH
default:
    printf ("%s\t%ld\n", getMnemonic(TRN_LOCAL), pItem->lTenderAmt);
    break;    // break from switch()
}

Note: Be aware that using a switch() to jump into a loop will bypass any loop initialization expression with some loop constructs such as for(). The loop conditional check expression will also be bypassed when jumping into the loop body. A similar note applies to an if statement should a case label be in the body of the if.

switch (i) {
    default: if (iVal > 3) {  // when i is not equal to 5 then iVal is checked
        case 5: iVal++;       // when i equals 5 then iVal is not checked
    }
}

or with a for() loop should the case label be inside the loop then the initialization is bypassed.

switch (i) {
    default: for (iVal= 0; iVal< 4; iVal++) {  // i not equal to 5 so iVal is initialized
    case 5:      doSomething (iVal);   // i equals 5 so iVal is not initialized
             }
        break;
}

The next unusual usage is to test for a sequence of values and perform some set of steps for those values. This uses the same data struct above. We could also have more than one line if we used curly braces. Without curly braces the first terminating semicolon also terminates the switch().

// in the case of a foreign tender perform the currency conversion
switch (pItem->uchMinorCode)
    case FOREIGN_1: case FOREIGN_2: case FOREIGN_3:
      pItem->lTenderAmt = pItem->lForeignCurrency * pItem->lRate;

A third unusual usage is to generate a set of values within a while() loop and, depending on the value, do something. This example is somewhat contrived to show syntax possibilities. I haven't seen anything like this in the wild however error check after calling a function and doing a retry based on the error code is a possibility.

while ((iVal = f(iVal))) {  // get next value in a sequence and do something with it.
    switch (iVal) {
        int j;        // define temporary variable used below.
    case 1:
        continue;     // continue the while() to get next value
    case 2:
        break;        // break from switch() to do printf() below
    case 0: do {
            j = 17;
            break;    // break from do()
    default:
            j = 12;
            break;    // break from do()
        } while (0);  // bottom of the do(). print follows
        printf("   do while - iVal = %d, j = %d\n", iVal, j);
        break;   // break from switch() to do printf() below
    }
    printf(" while - iVal = %d\n", iVal);
    break;       // break from while() loop
}

As a fourth example we use a switch() statement for testing various bits. Since a case must be an integral value which is computed at compile time we can use constants with bitwise operators if the compiler, and most modern ones will do so, will perform the calculation at compile time.

#define VAL_1  0x0001
#define VAL_2  0x0002
#define VAL_3  0x0004
#define VAL_4  0x0008

switch (iBitMask & 0x000f) {
case VAL_1:            // only VAL_1 is set
    printf("  only VAL_1 found\n");
    break;
case VAL_1 | VAL_2:    // both and only both VAL_1 and VAL_2 are set
    printf("  both VAL_1 and VAL_2 found\n");
    break;
case VAL_1 | VAL_3:    // both and only both VAL_1 and VAL_3 are set
    printf("  both VAL_1 and VAL_3 found\n");
    break;
case 0x000f & ~VAL_1:
    printf("  everything except for VAL_1 found\n");
    break;
}

References

How does switch statement work?

Does the C standard explicitly indicate truth value as 0 or 1?

Using continue in a switch statement

Upvotes: 1

mleko
mleko

Reputation: 12243

This is valid C code. It origins from assembly where every conditional statement uses if, goto and label jumps.

Implementation of array copy using this feature is called Duff's Device

void copy(char *from, char *to, int count)
{
    int n = (count + 7) / 8;

    switch(count % 8) {
        case 0: do { *to++ = *from++;
        case 7:      *to++ = *from++;
        case 6:      *to++ = *from++;
        case 5:      *to++ = *from++;
        case 4:      *to++ = *from++;
        case 3:      *to++ = *from++;
        case 2:      *to++ = *from++;
        case 1:      *to++ = *from++;
                   } while(--n > 0);
    }
}

When you replace while with if and goto

void copy(char *from, char *to, int count)
{
    int n = (count + 7) / 8;

    switch(count % 8) {
        case 0: 
        loop:        *to++ = *from++;
        case 7:      *to++ = *from++;
        case 6:      *to++ = *from++;
        case 5:      *to++ = *from++;
        case 4:      *to++ = *from++;
        case 3:      *to++ = *from++;
        case 2:      *to++ = *from++;
        case 1:      *to++ = *from++;
        if(--n > 0) goto loop;
    }
}

Then replace switch with if and goto

void copy(char *from, char *to, int count)
{
    int n = (count + 7) / 8;

    if(count%8==7)goto case7;
    if(count%8==6)goto case6;
    if(count%8==5)goto case5;
    if(count%8==4)goto case4;
    if(count%8==3)goto case3;
    if(count%8==2)goto case2;
    if(count%8==1)goto case1;
    if(count%8==0)goto case0; // this can be omitted

    case0:                    // this can be omitted
    loop:        *to++ = *from++;
    case7:       *to++ = *from++;
    case6:       *to++ = *from++;
    case5:       *to++ = *from++;
    case4:       *to++ = *from++;
    case3:       *to++ = *from++;
    case2:       *to++ = *from++;
    case1:       *to++ = *from++;

    if(--n > 0) goto loop;
}

Which is functionally (almost) equivalent to

void copy(char *from, char *to, int count)
{
    while(--n > 0) { 
        *to++ = *from++;
    }
}

It's almost equivalent because in the last implementation loop check is performed 8 times more often, what have impact on performance.

Upvotes: 2

Tony O
Tony O

Reputation: 513

You can look here for example of unusual usage of switch statement. But don't do this in a real code. Example from link:

int duffs_device(char *from, char *to, int count)
{
    {
        int n = (count + 7) / 8;

        switch(count % 8) {
            case 0: do { *to++ = *from++;
                        case 7: *to++ = *from++;
                        case 6: *to++ = *from++;
                        case 5: *to++ = *from++;
                        case 4: *to++ = *from++;
                        case 3: *to++ = *from++;
                        case 2: *to++ = *from++;
                        case 1: *to++ = *from++;
                    } while(--n > 0);
        }
    }

    return count;
}

It's called Duff's device. This function copies char array. It uses the trick called "loop unrolling".

Long loops can be slow because every iteration they need to do additional work like comparisons and variable incremention. So one way to speed them up is to duplicate repeating code. Like its done in example.

But modern compilers can do this better, and using code like this is not recommended, because it only confuse people who read it.

Upvotes: 1

Vlad from Moscow
Vlad from Moscow

Reputation: 311078

You can consider the switch statement as a code block with labels (case(s) are indeed labels) where the control is passed with a goto statement.

Something like

void f(int n)
{
    if ( n == 0 ) goto Label_case_0;
    else goto Label_default;

    {
        if ( 1 )
        {
            Label_case_0:
            printf("zero\n");
        }
        else 
            while (--n)
            {
                Label_default:
                printf("non-zero\n");
            }
    }
}

In my opinion it is not a good idea to place case labels inside some other control structures because this makes the code difficult to read and can lead to errors.

Upvotes: 3

Related Questions