That error even happens if you delete int a(), I'm guessing it treats a constexpr T(void) the same as a constant and pre-evaluates it.
EDIT: If you make it slightly more complicated so that constexpr_branch_fn uses an input value then the optimization is not done https://gcc.godbolt.org/z/fT689qeT9 (weirdly just giving it an input and not using it isn't enough, it must be looking at the body to determine whether to pre-evaluate the function or not).
A complication: RFC #3352 would allow the const-time and runtime behavior of const fn to be different. If that RFC is accepted and makes the equivalent of C++ if consteval an acceptable thing to do, it becomes incorrect to constant evaluate an expression which is not in a constant evaluated context.
It remains valid to constant propagate and optimize out untaken arms that way, but constant propagation is solely an optimization, must have the same result as if it had not happened, and applies uniformly to all functions; const fn are not special in any way in runtime evaluated contexts.
const blocks were introduced specifically to allow lifting arbitrary code into a constant evaluated context. Rvalue promotion which implicitly lifts runtime evaluated code to constant evaluated code is currently in the process of slowly being descoped down to code which is trivially known to be constant (i.e. just literal expressions, including struct literals, excluding any function calls).
But it could potentially also become slower by spending a bunch of time interpreting MIR.
Given a fast codegen backend -- like hopefully cranelift will be -- then it's probably faster to just emit the dead code rather than evaluate all the not-already-known-const conditions to see if they can be omitted.
As with so many things, I suspect there is no perfect answer. But I wouldn't be surprised if doing a bit more effort and work to try to emit less code would be good for compile times, and release compile times are important too, and those will be LLVM for the foreseeable future.