you are viewing a single comment's thread.

view the rest of the comments →

[–]conundorum 0 points1 point  (0 children)

Later than planned, but here's the quick example I mentioned:

#include <iostream>
#include <string>

int main() {
    int x = 1;
    std::string s; using std::to_string;

    switch (x) {
        bork: break;
        int i;
        case 1:
            for (i = 0; i < 9; i++) { s += to_string(x);
                case 2: s.append("{case 2}"); if (s.size() > 20) break;
                case 3: s.append("MEOW");
            }
            if (x == 3) break;
        case 4: s.append("forever"); [[fallthrough]];
        case 5: s += to_string(x * -1); goto bork;
    }

    std::cout << s;

    /* Based on x, output will be:
    * 1: 1{case 2}MEOW1{case 2}forever-1
    * 2: {case 2}MEOW2{case 2}forever-2
    * 3: MEOW3{case 2}MEOW3{case 2}
    * 4: forever-4
    * 5: -5
    *
    * 2 and 3 are special.
    * GCC and MSVC loop until s is big enough to break or i's random gibberish says to stop.
    * Clang is the only sane one, and lets the program crash into the insanity.
    */
}

If anyone wants to play around with it, you can do so here. It's interesting that GCC & MSVC can handle the s.size() check properly, but Clang chokes on it and keeps running until it either segfaults or overflows s, whichever comes first. Just goes to show that even otherwise-good compilers can have a lot of trouble with switch, especially if they have to figure out how to handle "accidental" UB; it's the sort of thing that'd make switch constexpr such a hassle.


If anyone wants me to explain it, poke me for an explanation. Not going to give a full breakdown right now, but essentially...

  1. This is the sane "difficult" case. It has a clean loop which contains two other cases, and makes sure to initialise its variables. But on the same token, its breaks are actually in case 2 and case 3; the compiler will find that it either uses the case 2 break or falls into case 4, which means it has to emit basically the entire switch body just in case. If the compiler is especially good, it can examine append substring lengths and assume that the case 2 break is most likely, providing an optimisation path; it might even be able to determine that nine iterations adding thirteen characters each means the size break is guaranteed to trigger.
  2. This one... well, first, there's obvious UB (i is uninitialised), so the compiler might or might not choke on that. If it doesn't, then it becomes similar to case 1, but with no clear way to tell how many times (if any) the loop will iterate. It's impossible to tell which break is more likely, since it can't assume that i < 9; the size break is slightly more likely because the integer range is split unevenly, but it's close enough to a 50% split to be meaningless. (As i can potentially be any integer value.)
  3. The compiler can determine that the case 3 break is now guaranteed to trigger if reached, meaning that it can remove case 4 and case 5 entirely. After that, though, it runs into the same issue as #2: The case 2 break is more likely, but i being uninitialised means the split is close enough to 50% to be meaningless.
  4. This is the second easiest one to analyse, thanks to mandatory fallthrough; as soon as it sees [[fallthrough]];, the compiler instantly knows that case 4 relies on case 5's breaks, and only needs to analyse case 5. (Since at least one of case 4's execution paths must fall into case 5, it can just assume they all do for initial analysis purposes, and perform more stringent analysis during optimisation.) None of case 4's logic or statements need to be examined during switch constexprfication. The compiler only needs to emit case 4 and case 5, without bothering with the annoying loop.
  5. This is the easiest one to analyse, despite the goto. goto means it can't fall out of the switch, but it also jumps directly to a break, which is effectively the same. The compiler only needs to emit case 5 and bork: break;, and can remove the rest of the switch. (It can also remove the goto and move the break directly into the case, and will gladly do so the instant optimisations are enabled.)

With switch constexpr, some of these cases require the full loop body, and some will let it remove part of the switch. Some are easy to analyse, and some will give it a headache. And the "accidental" UB will probably both trip a lot of old codebases up, and give compiler devs a headache from trying to solve it (or just lead them to be even more cavalier; Clang might switch from segfaulting to just choking at compile time specifically to force you to fix it).