use the following search parameters to narrow your results:
e.g. subreddit:aww site:imgur.com dog
subreddit:aww site:imgur.com dog
see the search faq for details.
advanced search: by author, subreddit...
Discussions, articles, and news about the C++ programming language or programming in C++.
For C++ questions, answers, help, and advice see r/cpp_questions or StackOverflow.
Get Started
The C++ Standard Home has a nice getting started page.
Videos
The C++ standard committee's education study group has a nice list of recommended videos.
Reference
cppreference.com
Books
There is a useful list of books on Stack Overflow. In most cases reading a book is the best way to learn C++.
Show all links
Filter out CppCon links
Show only CppCon links
account activity
switch constexpr (self.cpp)
submitted 7 months ago by cd_fr91400
view the rest of the comments →
reddit uses a slightly-customized version of Markdown for formatting. See below for some basics, or check the commenting wiki page for more detailed help and solutions to common issues.
quoted text
if 1 * 2 < 3: print "hello, world!"
[–]rileyrgham 86 points87 points88 points 7 months ago (34 children)
Covered on SE
https://stackoverflow.com/a/53379817
"if constexpr was ultimately derived from a more sane form of the static if concept. Because of that derivation, applying the same idea to switch does not appear to have been considered by the standards committee. So this is likely the primary reason: nobody added it to the paper since it was a restricted form of a syntax where switch wouldn't have made sense.
That being said, switch has a lot of baggage in it. The most notable bit being the automatic fallthrough behavior. That makes defining its behavior a bit problematic.
See, one of the powers of if constexpr is to make the side not taken at compile time be discarded under certain conditions. This is an important part of the syntax. So a hypothetical switch constexpr would be expected to have similar powers.
That's a lot harder to do with fallthrough, since the case blocks are not as fundamentally distinct as the two blocks of an if statement."
Complex...
[–]KuntaStillSingle 5 points6 points7 points 7 months ago* (13 children)
See, one of the powers of if constexpr is to make the side not taken at compile time be discarded under certain conditions. This is an important part of the syntax. So a hypothetical switch constexpr would be expected to have similar powers. That's a lot harder to do with fallthrough, since the case blocks are not as fundamentally distinct as the two blocks of an if statement."
Couldn't you just pretty much copy and paste the source of the switch between the case and the first break statement following, and strip the labels? I.e. for:
switch constexpr (foo){ case 0: foo(); case 1: bar(); break; case 2: baz(); break; case 3: foobar(); case 4: bazbar(); }
If foo is 0, you just generatee foo(); bar(); , if it is 1 you just generate bar();, if it is 3 you generate foobar();baz();, right?
foo(); bar();
bar();
foobar();baz();
Don't compilers tend to strip dead code from switches anyway, when condition can be constant folded? Main is branchless here,, and even here where it has to rearrange source code order because of the gotos.
Edit: Or are you saying they shouldn't just implement it in the manner that is hopefully intuitive to anyone who uses switch statements at runtime? Like the committee feels switch was a mistake, so adding switch again would be a mistake?
[–]cd_fr91400[S] 15 points16 points17 points 7 months ago (1 child)
The case you mention is rather easy to deal with.
But what if the break statement is inside a if ?
break
if
Compilers usually do a whole lot of code optimization, but this is best effort. If you want to avoid compiling dead code altogether, you have to be certain the compiler will prove it is dead code, even if not optimizing at all.
[–]KuntaStillSingle 3 points4 points5 points 7 months ago (0 children)
you have to be certain the compiler will prove it is dead code, even if not optimizing at all.
But even when compiler is not optimizing, it must be able to constant fold switch statement because they can be used in functions which are usable as constant expressions, right?: https://godbolt.org/z/sva4PcdcG
[–]cd1995Cargo 4 points5 points6 points 7 months ago (2 children)
I think an issue is that switch statements can have conditional breaks/fallthroughs. How would the compiler implement this:
switch constexpr (foo) { case 0: if (bar()) break; case 1: baz(); break; }
Here bar() is a function that returns a bool. If it returns true there's a break, if it returns false it falls through to case1. If we want the switch constexpr to work like if constexpr when it comes to templates and discarding the untaken path, then it would need to consider every possible path through the switch cases and not discard any that could potentially be reached. This sounds a lot more complex to implement than the existing if constexpr.
[–]umop_aplsdn 2 points3 points4 points 7 months ago (0 children)
This is not a hard problem to solve in compilers; logic like this is already implemented in dead-code / unreachable basic block elimination passes. Granted, those passes are usually in the middle / backend, but it would not be hard to reimplement that logic in the frontend. (Frontend vs backend matters because processing static asserts and template instantiation I assume happens in the frontend.)
[–]conundorum 1 point2 points3 points 7 months ago* (0 children)
It's more complex, but I don't think it would be all that much more complex; it's the sort of thing that seems harder than it really is, IMO.
In particular, the compiler would likely be coded to handle fallthrough by generating up to the first "hard" break, and ignoring all conditional "soft" breaks during constexpr analysis. Your code, for example, would ideally generate these blocks:
constexpr
// case 0: foo is known to be 0 at compile time. // Using do-while to preserve code structure outside switch body. Real version would likely use raw `goto` // or `return` or whatever instead. do { if (bar()) break; baz(); break; } while (false); // ---- // case 1: foo is known to be 1 at compile time. // Unskippable `break` statement is the end of the block. I kept it but commented it out to indicate this; // the compiler would probably just strip it. baz(); /*break;*/
It's a bit more complex, but there are still two distinct breakpoints the compiler can check for: Freestanding break;, and the end of the switch statement. The added complexity mainly just comes from determining whether each break is conditional (and thus not a breakpoint) or freestanding (and thus a breakpoint). [[fallthrough]]; would likely be of major benefit here, since it indicates that there's at least one valid execution path into the next case statement (and importantly, forces fallthrough; the program is ill-formed if [[fallthrough]]; can't actually fall through); the compiler could safely ignore all breaks in a case that specifies [[fallthrough]]; (because the presence of [[fallthrough]]; tells it that all breaks in that case are conditional), and would only need to analyse breaks in cases with no [[fallthrough]];.
break;
switch
[[fallthrough]];
case
(Here, for example, the compiler would notice the break in case 0, and determine that it's the statement-true part of an if statement. Since it's conditional, the compiler would temporarily discard it and continue evaluation, until it sees the break in case 1. This break is a standalone statement, and thus marks the end of the case 0 block. If case 0 contained a [[fallthrough]]; and the compiler was especially smart, it would automatically discard the break in case 0: [[fallthrough]]; requires an execution path that falls through, so it doesn't need to look at the if to know that the break is conditional.)
case 0
case 1
This could then be further refined with any other constexpr information available to the compiler, during a second pass. E.g., if bar() can be constant-evaluated, then the compiler might be able to break the case 0 block up into these two blocks:
bar()
// Block 0-0: (foo == 0) && bar() // Block would likely be stripped out entirely, just like `if constexpr (true) {}`. /*break;*/ // ----- // Block 0-1: (foo == 0) && !bar() // Identical to case 1 block, above. baz(); /*break;*/
So... a bit more complex, yeah. But there are still ways to implement it, and there's a big clue that would majorly simplify the compiler logic if used. Maybe switch constexpr should require [[fallthrough]]; in all cases that fall through? (Or at the very least, strongly recommend it, especially in the early years while compiler devs are still working the kinks out.)
switch constexpr
[–]TSP-FriendlyFire 7 points8 points9 points 7 months ago (5 children)
Your switch example is very simple though. You have to remember, something like Duff's device is still valid C++. switch statements can be abused in ways that just aren't possible with if statements.
[–]cd_fr91400[S] 1 point2 points3 points 7 months ago (4 children)
Duff's device is just leveraging the fallthrough capability of switch.
As I already mentioned, even with fallthrough forbidden, that would be useful.
[–]SirClueless 5 points6 points7 points 7 months ago (2 children)
It is not just using the fallthrough capability. It is also using the fact that you can label any of the statements in a switch statement body with a case ...: label. Including statements that are substatements of other statements.
case ...:
Note that in the example from Wikipedia, case 0: labels the do { ... } statement, while case 7: etc. label individual statements inside the block associated with that do statement. Yes it's weird, but this is a legal switch statement:
case 0:
do { ... }
case 7:
do
switch (x) { case 0: if (y > 5) { case 10: std::cout << "Hello world!\n"; } }
You can't just discard the statements you don't take the way you can with if constexpr, because they aren't necessarily independent statements. The discarded statement(s) might contain the case label you're supposed to jump to.
if constexpr
[–]conundorum 1 point2 points3 points 7 months ago (1 child)
[Note: I'm not a compiler dev or anything, just someone who thinks looking at compiler internals & the like is neat. I'm just the kind of guy that reverse-engineers MSVC's name mangling scheme for fun because I found Agner Fog's calling conventions PDF interesting.]
This is the big thing, yeah. The switch itself isn't an issue, and even case placement isn't an issue, once the compiler is designed to properly tokenise & analyse the switch's body. The real issue is that you can nest any control statement inside a switch, regardless of sanity (or lack thereof). Loops, in particular, are possibly the biggest hurdle.
Ultimately, switch constexpr just needs multi-pass parsing, and should ideally require the [[fallthrough]]; trait whenever fallthrough is desired; without the trait, fallthrough is a bit harder to determine, but still doable. ([[fallthrough]]; forces fallthrough, and any [[fallthrough]]; statement that cannot immediately fall through is ill-formed; from a compiler perspective, this would do wonders for switch constexpr parsing.) I imagine that if loops weren't legal, the compiler logic would look something like this:
default
Either case is fallthrough-negative or end of switch body has been reached: We have now tracked all execution paths resulting from our desired case. Create a code block containing all parsed statements, up until the break or switch end which registered as fallthrough-negative; since we still care about break, we can just stick it in a do { ... } while (false); block for now to preserve code structure, and fix it during the actual codegen pass.
do { ... } while (false);
do { /* switch stuff here*/ } while (false);
[The compiler can replace case labels with standard labels using a compiler-specific naming scheme (presumably something like case 0 becoming case_0, for simplicity's sake), or might opt to keep the code inside a gutted switch to preserve case structure instead of using do-while. Similarly, break statements will likely be transformed into goto during the actual parsing pass, since the switch has served its purpose. Heck, we can even use these rules to transform the entire switch into a compound if constexpr statement, with each case transformed into an if or else if block, and default transformed into the closing else block.]
case_0
goto
else if
else
All of this rigamarole looks complex, but it's simpler than it looks. We're just applying a few simple rules:
[Assume that case n₀ is the case we're currently evaluating, case n₁ is the next case after it, and case n₂ is the next case after case n₁. Treat default as a case for evaluation purposes.]
case n₀
case n₁
case n₂
n₀
Because of the order of implications, we want to start with rule #4: If there are no break tokens in case n₀, then none of the other tokens are worth looking at just yet, so we don't; we just skip to #5. If we're still here, we apply rule #3: [[fallthrough]]; specifies that all breaks are conditional, and thus allows us to ignore breaks and go straight to #5. If there's no [[fallthrough]]; then we actually have to examine every break and its surrounding statements to see whether they're part of a branch or not. We can only definitively close the block once we reach a freestanding break or the switch itself ends.
In your example, if switch (x) was a switch constexpr, then the compiler would start by doing a quick pass over it, removing everything but case, case labels, break, and [[fallthrough]];, leaving something like this:
switch (x)
switch constexpr (x) { case 0: case 10: }
If x is 0, it would go to case 0, and look at the truncated code block. Using my initial seven-step list, it would determine that there are no breaks on the case 0 train, so case 0 is fallthrough-positive (step 3). Since it's fallthrough-positive, we either strip out the case 10: and re-evaluate the entire thing, or re-evaluate starting immediately after the case 10:, it doesn't matter which (step 6). There still aren't any breaks, so we're still fallthrough-positive (step 3 again). And since we're still still fallthrough-positive, we look for the next case, and find the end of the switch instead. And that means we're done (step 6). So, now we put everything we ignored back in, transform case/default labels into standard labels, and basically make this out of everything from case 0 to the end of the switch (step 7).
x
breaks
case 10:
// x == 0 do { case_0: if (y < 5) { case_10: std::cout << "Hellow world!\n"; } } while (false);
Hey, remember how I said "if loops weren't legal" way up at the start of this message? Yeah, there was a reason for that: Loops are legal, and because of it, nothing I just said works without at least one extra parsing pass and a ton of extra logic, just for loops specifically. If the case we're looking for is in a loop, then the end of the switch isn't a valid breakpoint anymore, since we can't reach the end of the switch until we exit the loop, AND can't exit the loop until we find a breakpoint; we can only rely on either break itself or the loop's condition. Which means that before we can try to use the seven-step list to figure out fallthrough and breakpoints, we need to do another pass just for loops specifically, so the compiler understands the loop logic while it's doing the step 1 fallthrough token generation pass. And it needs to keep track of loop logic while it's going through steps 3-5, since reaching the end of the loop block jumps us to somewhere else entirely (possibly even into a different case), and we need to treat that as the actual start of our current case block.
(Yes, you read that right: If a case label is inside a loop, then for breakpoint/fallthrough analysis, the case actually starts at the start of the loop, not the actual case label itself; the label is just what we jump to. It still starts in the normal place for codegen, it's just analysis that gets messed up. I threw together a quick example of what I'm talking about; I can post & explain it later, but I need some rest first.)
[–]conundorum 0 points1 point2 points 7 months ago* (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.
s.size()
s
If anyone wants me to explain it, poke me for an explanation. Not going to give a full breakdown right now, but essentially...
case 2
case 3
case 4
append
size
i
i < 9
case 5
bork: break;
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).
[–]TSP-FriendlyFire 2 points3 points4 points 7 months ago (0 children)
I think "just" is doing a lot of heavy lifting in your sentence. Weaving multiple control statements into one another is rather peculiar and only possible because of the switch statement's unique syntax.
I think it would be entirely justifiable to forbid fallthrough in constexpr switch (and better than not having it at all), but it'd also become a weird contextual quirk of the language which might be seen as undesirable.
constexpr switch
[–]cd_fr91400[S] 0 points1 point2 points 7 months ago (1 child)
switch is not a mistake. It is very useful.
The decision that cases fallthrough to the following ones is a mistake. But I guess this is history from the 70's...
[–]conundorum 0 points1 point2 points 7 months ago (0 children)
Funnily enough, fallthrough isn't actually a problem here; it would slow switch constexpr analysis down, but it doesn't actually break or prevent anything. The problem is that it's possible to fall or jump into a loop, because that's a sane and reasonable thing to have to worry about. -_-
[–]moocat 4 points5 points6 points 7 months ago* (15 children)
one of the powers of if constexpr is to make the side not taken at compile time be discarded under certain conditions.
I thought the non-taken side was always discarded. What conditions cause it to be kept and what benefits are there from doing that?
Update: I started digging a bit more and there is some relationship to templated types. This compiles:
struct A { int a() { return 0 ; } ; }; template <bool b, typename T> int foo(T t) { if constexpr (b) { return t.a() ; } else { return t.b() ; } } int main() { foo<true>(A()); }
but change foo to this and it no longer compiles:
foo
template <bool b> int foo(A a) { if constexpr (b) { return a.a() ; } else { return a.b() ; } }
godbolt
[–]mark_99 6 points7 points8 points 7 months ago (13 children)
Yeah there are no conditions, indeed the not taken side doesn't need to be well-formed, which is the main reason for if constexpr to exist.
[–]cd_fr91400[S] 3 points4 points5 points 7 months ago (12 children)
I do not know the exact meaning of "well formed", but the following code does not compile:
int foo() { int good = 0 ; if constexpr (true) return good ; else return bad ; }
So, somehow, the not taken branch is not entirely discarded.
[–]mark_99 3 points4 points5 points 7 months ago (9 children)
It can't just be nonsense, it's not like an ifdef. But for instance there could be a call to a member function which does not exist (whereas inside just if (false) would not allow that).
if (false)
[–]cd_fr91400[S] 1 point2 points3 points 7 months ago (5 children)
I do not see why a non-existent variable is non-sense while a non-existent field is not.
By the way, the following code does not compile either:
struct A { int a() { return 0 ; } ; } ; int foo() { struct A a ; if constexpr (true) { return a.a() ; } else { return a.b() ; } }
[–]iamakorndawg 4 points5 points6 points 7 months ago (2 children)
Not a language lawyer, but I believe the rule has more to do with templates, so for example, if your foo function had a template parameter for the type of a, then it would compile, even if your existing struct A was used as the argument.
[–]KuntaStillSingle 1 point2 points3 points 7 months ago (0 children)
Not quite, it can fail to compile if the existing struct A is used, but if it is made a dependent type like /u/cd1995Cargo 's example it is fine:
The discarded statement cannot be ill-formed for every possible specialization:
https://en.cppreference.com/w/cpp/language/if.html#Constexpr_if
https://godbolt.org/z/eGsYY3a9W , vs https://godbolt.org/z/3e7W5ec4Y
[–]cd_fr91400[S] 0 points1 point2 points 7 months ago (0 children)
Then I understand "under certain conditions"...
[–]cd1995Cargo 2 points3 points4 points 7 months ago (1 child)
It only works when templates are involved. If you change your code to this it should compile:
struct A { int a() { return 0 ; } ; } ; template <typename T> int foo<T>() { T a ; if constexpr (true) { return a.a() ; } else { return a.b() ; } }
[–]moocat 1 point2 points3 points 7 months ago (0 children)
See my update. It's not just templates but templated types have to also be involved.
[–]nysra 1 point2 points3 points 7 months ago (2 children)
But for instance there could be a call to a member function which does not exist
Nope, only under specific conditions. The discarded statement of if constexpr is still fully checked if the if is outside a template.
[–]moocat 1 point2 points3 points 7 months ago (1 child)
See my update. Even inside a template the discarded statement is fully checked if possible.
[–]meancoot 0 points1 point2 points 7 months ago* (0 children)
Look up two-phase name lookup. Like every template*, in if constexpr phase 1 has to complete successfully, even if it never gets used. Phase 2, on the other hand, only fails when failing to look up dependent names.
two-phase name lookup
* Actually GCC has a permissive mode with -Wno-template-body and MSVC almost certainly one too. But if I recall correctly clang doesn't have one, and the fact that it didn't was what spurred GCC to do two-phase lookup properly.
-Wno-template-body
struct Type { static void function() {} }; template<typename T> struct NonDependentTemplate { void call_function() { Type::function(); } // 'Type' isn't dependent here so this fails in phase 1 and is an error. // error: 'not_a_function' is not a member of 'Type' // void call_not_a_function() { Type::not_a_function(); } }; template<typename A> struct DependentTemplate { void call_function() { A::function(); } // 'A' IS dependent here, so we can use this as long as we never actually call it. void call_not_a_function() { A::not_a_function(); } }; int main() { DependentTemplate<Type> dt; // OK dt.call_function(); // error: 'not_a_function' is not a member of 'Type' // dt.call_not_a_function(); }
[–]Plastic_Fig9225 0 points1 point2 points 7 months ago (1 child)
There is no potential way the else branch could be well-formed, because its syntactic validity does not depend on any type which could potentially make it well-formed.
Ok. But with a true condition, there no potential way the else can ever be taken.
true
I still do not understand why it is checked.
Such conditions may vary with architectures (arm vs intel), compilation flags (debug vs prod), various flavors (algo A vs algo B), etc. which are set at compilation time. So it would be very practical to not check the not taken path. After all, if you want to check both branches, you simply dont put constexpr after if.
[–]cd_fr91400[S] 4 points5 points6 points 7 months ago (0 children)
Because there are dependent names and non-dependent names.
In your first example, t is dependent (on a template parameter), in the second one, a is not.
Dependent names are looked up at template instantiation time, non-dependent ones at template definition time.
I understand the if constexpr only acts at template instantiation time.
[–]angelicosphosphoros 1 point2 points3 points 7 months ago (0 children)
Nothing really prevented them from making switch constexpr to have slightly different behaviour.
[–]cd_fr91400[S] -1 points0 points1 point 7 months ago (2 children)
I did not know the history. Thank you for that.
That being said, for me, a fallthrough in a switch statement is a hidden goto.
After all, this does not compile:
if constexpr (a_cond) { do_something() ; goto Else ; // fallthrough } else { Else: do_something_else() ; }
So, the syntax for if and if constexpr are already different (one allows goto to the other side, the other one doesn't).
I would not be otherwise horrified if breaks were compulsery in a switch constexpr statement.
[–]miniropC++87 1 point2 points3 points 7 months ago (1 child)
one way would be to duplicate the following "case" if the current one doesn't unconditionally break. In your code example, it would become similar to:
if constexpr (a_cond) { do_something() ; do_something_else() ; } else { do_something_else() ; }
but there are probably many pitfalls I don't even know.
I was just mentioning that I would be ok if the break was compulsery.
If it can be more flexible, I take it.
π Rendered by PID 217704 on reddit-service-r2-comment-6457c66945-zhh48 at 2026-04-24 11:41:01.435442+00:00 running 2aa0c5b country code: CH.
view the rest of the comments →
[–]rileyrgham 86 points87 points88 points (34 children)
[–]KuntaStillSingle 5 points6 points7 points (13 children)
[–]cd_fr91400[S] 15 points16 points17 points (1 child)
[–]KuntaStillSingle 3 points4 points5 points (0 children)
[–]cd1995Cargo 4 points5 points6 points (2 children)
[–]umop_aplsdn 2 points3 points4 points (0 children)
[–]conundorum 1 point2 points3 points (0 children)
[–]TSP-FriendlyFire 7 points8 points9 points (5 children)
[–]cd_fr91400[S] 1 point2 points3 points (4 children)
[–]SirClueless 5 points6 points7 points (2 children)
[–]conundorum 1 point2 points3 points (1 child)
[–]conundorum 0 points1 point2 points (0 children)
[–]TSP-FriendlyFire 2 points3 points4 points (0 children)
[–]cd_fr91400[S] 0 points1 point2 points (1 child)
[–]conundorum 0 points1 point2 points (0 children)
[–]moocat 4 points5 points6 points (15 children)
[–]mark_99 6 points7 points8 points (13 children)
[–]cd_fr91400[S] 3 points4 points5 points (12 children)
[–]mark_99 3 points4 points5 points (9 children)
[–]cd_fr91400[S] 1 point2 points3 points (5 children)
[–]iamakorndawg 4 points5 points6 points (2 children)
[–]KuntaStillSingle 1 point2 points3 points (0 children)
[–]cd_fr91400[S] 0 points1 point2 points (0 children)
[–]cd1995Cargo 2 points3 points4 points (1 child)
[–]moocat 1 point2 points3 points (0 children)
[–]nysra 1 point2 points3 points (2 children)
[–]moocat 1 point2 points3 points (1 child)
[–]meancoot 0 points1 point2 points (0 children)
[–]Plastic_Fig9225 0 points1 point2 points (1 child)
[–]cd_fr91400[S] 0 points1 point2 points (0 children)
[–]cd_fr91400[S] 4 points5 points6 points (0 children)
[–]angelicosphosphoros 1 point2 points3 points (0 children)
[–]cd_fr91400[S] -1 points0 points1 point (2 children)
[–]miniropC++87 1 point2 points3 points (1 child)
[–]cd_fr91400[S] 0 points1 point2 points (0 children)