Search code examples
c++scopeswitch-statementbraces

c++ switches Return inside/outside braces? break?


In c++ you don't have to put braces {} inside cases in switch statements unless you're declaring a variable.

Consider the following code:

int Lesson82a() {
    int test = 1;
    switch (test) {
    case 1:
    {
        int i = 1;
        cout << i << "\n";
        cout << "Inside braces" << "\n";
        return 0;
    }
    case 2:
        cout << "2" << "\n";
    case 3:
        cout << "3" << "\n";
    }
    return -1;
}

int Lesson82b() {
    int test = 1;
    switch (test) {
    case 1:
    {
        int i = 1;
        cout << i << "\n";
    }
    cout << "Outside braces" << "\n";
    return 0;
    case 2:
        cout << "2" << "\n";
    case 3:
        cout << "3" << "\n";
    }
    return -1;
}

void Lesson82() {
    int test = Lesson82a();
    cout << test << "\n";
    test = Lesson82b();
    cout << test;
}

When running Lesson82() the output is as follows:

1
Inside braces
0
1
Outside braces
0

So the result is the same but does it make a difference?

  1. Should i put braces always wheather i declare a variable or not?
  2. If i put braces, does it matter if i put all my code for the case inside the braces? or only the code that needs the variable(s) i declare such as in Lesson82b() where i run some code and a return statement after the braces.
  3. Should i put break; after return even though it's then unreachable?

Does it make a difference when it is compiled?

On line 122 here: https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/begin/LearnWin32/BaseWindow/cpp/main.cpp#L122 i couldn't understand why the return statement is outside the braces.

I tried experimenting but i get the same results in my code regardless of it.


Solution

  • The switch statement’s cases do not require braces, period.

    The purpose of braces is to create a local lexical context — that is, a space where one or more local variables may be created (and consequently given a specific, limited lifetime). In the case of a case clause in a switch statement, that lifetime must be limited. Consider:

    switch (quux)
    {
      case 1:
        int n = quux / 2;
        break;
      case 2:
        std::cout << "Hello " << n << "!\n";
        break;
    }
    

    If quux is 2, then what is the expected lifetime of n? Is it even created? When we attempt to print the value of n, is there even an n to print?

    The answers are no. The targeted switch-case label jumps right over the code that creates it.

    In C (before C23), it is a syntactic failure as well: the target of case labels must be a statement. But int n ... is a variable declaration.
    Sorry, I misread your language tag as C. But I’ll leave this here since it is worthwhile info for C anyway.


    We could move the declaration to before the switch. Let’s try it:

    int n;
    switch (quux)
    {
      case 1:
        n = quux / 2;
        break;
      case 2:
        std::cout << "Hello " << n << "!\n";
        break;
    }
    

    Now when quux is 2 we jump right over the code that initializes n with a value. With your compiler warnings turned up, you should definitely see something like:

    error: 'n' may be used uninitialized
    

    ...which is UB.


    These two examples are bad code. (IOW, don’t do that.) They exist solely to demonstrate the problem.

    But suppose you have a construct like this:

    int n;
    switch (quux)
    {
      case 1:
        n = quux / 2;
        std::cout << n << "\n";
        break;
      case 2:
        std::cout << "Hello world!\n";
        break;
    }
    

    Surely life is better, right?

    Maybe. But the problems already noted still exist. You cannot declare n local to where it is used, and in those cases where it is not used, it is uninitialized. I cannot say whether or not this is actually consequential, but it is still something the compiler must think about, so it is.

    Aaaand, it is a good idea anyway to make local variables only exist when and for as little time as needed.


    The solution: create a local context using { curly braces }. This has always been how it works in C and, consequently, C++. It has always been totally valid to do something like:

    int main(void)
    {
      int sum = 0;             // x does not exist
      {                        // (new local context)
        int x = 10;            // x exists!
        while (x) sum += x--;  // x exists!
      }                        // (end of scope: x is destroyed)
      printf( "%d\n", sum );   // x does not exist
      return 0;                // x does not exist
    }
    

    We can apply this knowledge to use in switch statements.

    switch (quux)
    {
      case 1:
        {
          int n = quux / 2;
          std::cout << n << "\n";
        }
        break;
      case 2:
        std::cout << "Hello world!\n";
        break;
    }
    

    This is perfectly fine and valid. The case clauses only contain statements. n only exists in a local context. Everything is clear and, importantly, easy for the compiler (and us humans) to reason about.


    Now, return and break statements are attached to very specific brace-enclosed contexts. The return is attached to a function’s brace-enclosed context. The break is attached to a switch (or a loop’s) context. (It is not attached to a case!)

    That means that the position of return or break relative to a local brace-enclosed context is irrelevant. Both are valid:

    switch (quux)                       |  switch (quux)
    {                                   |  {
      case 1:                           |    case 1:
      {                                 |      {
        int n = quux / 2;               |        int n = quux / 2;
        std::cout << n << "\n";         |        std::cout << n << "\n";
        break;                          |      }
      }                                 |      break;
      case 2:                           |    case 2:
        std::cout << "Hello world!\n";  |      std::cout << "Hello world!\n";
        break;                          |      break;
    }                                   |  }
    

    In both of these cases the local context is properly terminated before the break jumps control to the end of the entire switch statement.


    The list of statements following a case label are like any other list of statements: they may contain brace-enclosed local contexts in any fashion you wish.

    switch (quux)
    {
      case 1:
        {
          std::cout << "inside\n";
        }
        std::cout << "outside\n";
        {
          std::cout << "inside again, lol\n";
        }
        break;
    }
    

    Just... try to write readable code. This leads us to the final bit:


    Brace-enclosed spaces are easily elided by the compiler when they serve no purpose. So using braces for stylistic reasons is fine:

    switch (quux)
    {
      case 1:
      {
        int n = quux / 2;
        std::cout << n << "\n";
        break;
      }
      case 2:
      {
        std::cout << "Hello world!\n";
        break;
      }
    }